diff --git a/README.md b/README.md index 4b3c61f..3aacd20 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ ### 核心特性 -* **模块化插件架构**:所有功能都在 `plugins/` 目录 +* **模块化插件架构**:所有功能都在 `plugins/` 目录,开发者可轻松扩展 * **极致性能优化**: - * **Python 3.14 JIT**:pypy不支持那个浏览器扩展我只能用JIT了。。。 - * **Mypyc 编译**:一些核心模块已经编译成机器码了 - * **Playwright 页面池**:浏览器页面预热池 + * **Python 3.14 JIT**:运行时热点代码编译成机器码 + * **Mypyc AOT编译**:核心模块编译为C扩展 + * **Playwright 页面池**:浏览器页面预热池,降低截图延迟 * **全局连接复用**:HTTP 和 Redis 连接池化管理 -* **开发者友好**:完整的类型提示,清晰的 API 设计。 -* **集成 Redis 缓存**:能缓存的都缓存了。群信息、用户信息、帮助图片 -* **正向 WebSocket 连接**:我只会支持正向WS连接。。。不要提意见,我不会听的。。。 +* **开发者友好**:完整的类型提示,清晰的 API 设计 +* **集成 Redis 缓存**:缓存帮助图片、权限数据、会话状态等 +* **正向 WebSocket 连接**:仅支持正向WS连接模式(Bot主动连接OneBot) ### 技术栈 @@ -42,24 +42,40 @@ ``` . ├── plugins/ # 插件目录,业务逻辑都在这 -│ ├── admin.py # 管理员指令 -│ ├── bili_parser.py # B站解析 (高性能版) -│ ├── code_py.py # 代码沙箱 -│ ├── echo.py # 复读机 -│ ├── forward_test.py # 合并转发测试 -│ ├── jrcd.py # 今日运势 -│ └── thpic.py # 东方图片 +│ ├── admin.py # 权限管理(Admin/User两级权限) +│ ├── auto_approve.py # 自动同意好友请求和群邀请 +│ ├── bot_status.py # Bot运行状态查询(图片形式) +│ ├── broadcast.py # 管理员专用广播功能 +│ ├── code_py.py # Python代码沙箱执行 +│ ├── echo.py # Echo/点赞功能 +│ ├── furry.py # Furry图片获取 +│ ├── github_parser.py # GitHub仓库链接解析 +│ ├── jrcd.py # 今日人品/长度查询 +│ ├── thpic.py # 东方Project随机图片 +│ ├── web_parser/ # Web链接解析系统(B站、抖音、GitHub等) +│ └── sync_async_test_plugin.py # 异步同步混用测试插件 ├── core/ # 框架核心,非请勿动 │ ├── api/ # OneBot API 封装 -│ ├── managers/ # 各种管理器 (指令, 浏览器, 图片, 插件) +│ ├── handlers/ # 事件处理器 +│ ├── managers/ # 各种管理器 (指令, 浏览器, 图片, 插件, 权限) │ ├── utils/ # 工具函数 │ ├── ws.py # WebSocket 通信层 (已编译) -│ └── bot.py # Bot 实例 +│ ├── bot.py # Bot 实例 +│ ├── config_loader.py # 配置加载 +│ └── permission.py # 权限枚举 ├── data/ # 数据存储 │ ├── admin.json # 管理员名单 │ └── permissions.json # 权限配置 -├── templates/ # Jinja2 模板 -├── setup_mypyc.py # 编译脚本 +├── models/ # 数据模型 +│ ├── events/ # OneBot事件模型 +│ ├── message.py # 消息段模型 +│ ├── sender.py # 发送者信息 +│ └── objects.py # API响应对象 +├── templates/ # Jinja2模板(用于图片生成) +├── docs/ # 开发文档 +├── tests/ # 单元测试 +├── setup_mypyc.py # Mypyc编译脚本 +├── config.toml # 配置文件 └── main.py # 启动入口 ``` diff --git a/docs/core-concepts/architecture.md b/docs/core-concepts/architecture.md index 9d42c90..d1c1dc7 100644 --- a/docs/core-concepts/architecture.md +++ b/docs/core-concepts/architecture.md @@ -1,59 +1,81 @@ -# 骨架 +# 架构设计 -Neobot是面向内部开发者的,我会开源,但是写的很烂。。。 +NEO Bot 是一个现代化的、高性能的异步 QQ 机器人框架。本文介绍其核心架构和设计理念。 -## 1. 动力核心 +## 1. 性能优化体系 -### Python 3.14 + JIT -镀铬酸钾创项目的时候用的 Python 3.14 3.14兼容JIT,那就这样吧 -* **何原理**: 运行时把热点代码编译成机器码(Just-In-Time) -* **何用途**: 密集CPU运算能提升一些,尤其是插件里的循环和函数调用 -* **怎么开**: 启动时加 `-X jit` 参数 +### Python 3.14 JIT(Just-In-Time 编译) -### Mypyc 编译 (AOT) -光 JIT 还不够。。核心模块(`core/ws.py`, `core/managers/*.py`)我编译成了C扩展 -* **何原理**: 因为这个项目有很多类型提示,然后我就编译成C库了。。。 -* **何用途**: WS和manager下边的模块都是机器码运行,或许会快一些。。。 +**原理**:Python 3.14 内置 JIT 编译器,运行时将高频调用的代码编译成机器码。 -### 异步 IO 模型 -* **Linux**: uvloop -* **Windows**:IOCP - * *注*: `winloop` 死了,会和面具打架。。。 +**适用场景**: +- 插件业务逻辑(循环、函数调用密集) +- 消息处理流程 -## 2. 连接模式 +**启用方法**: -### 正向 WebSocket 模式 -这是一种简单直接的模式 - -* **主动出击 (Client)**: Bot 是个客户端 - * **好处**: 你电脑能上网就行(实际上是因为没公网ip哈。。。) - -```mermaid -graph LR - subgraph Local [你的电脑/服务器] - Bot[NEO Bot] - Browser[Playwright 页面池] - end - - subgraph Remote [外部] - NapCat[NapCatQQ] - end - - Bot -- "WebSocket (主动连接)" --> NapCat - Bot -- "内部调用" --> Browser +```bash +python -X jit main.py ``` -## 3. 资源管理 +预期性能提升:2-5 倍(取决于代码热点)。 + +### Mypyc 编译(AOT - Ahead-Of-Time) + +**原理**:将类型注解的 Python 代码编译为 C 扩展,生成平台相关的二进制文件。 + +**编译范围**: +- `core/ws.py` - WebSocket 通信 +- `core/managers/` - 各种管理器 +- `core/api/` - API 封装 +- `models/` - 数据模型 + +**启用方法**: + +```bash +python setup_mypyc.py build_ext --inplace +``` + +预期性能提升:3-10 倍(核心模块)。 + +**注意**:编译产物平台相关,必须在目标环境编译。 + +### 异步 IO 模型 + +**Linux**:`uvloop`(libev 绑定,比 asyncio 快 2-4 倍) +**Windows**:IOCP(Windows 原生高性能 IO) + +## 2. 连接架构 + +### 正向 WebSocket 连接 + +NEO Bot 采用**正向 WebSocket 连接**模式:Bot 主动连接 OneBot 实现(如 NapCatQQ)。 + +**流程**: + +``` +Bot 启动 → 连接到 NapCatQQ (ws://127.0.0.1:3001) + ↓ + 监听消息事件 + ↓ + 分发到处理器 + ↓ + 调用 API 回复 +``` + +## 3. 资源管理架构 ### 单例管理器 -所有东西(指令、权限、浏览器、图片)都是全局独一份的。 -* **随叫随到**: 在哪都能直接 `import` -* **绝对权威**: 全局就一份数据 -### 资源池化 -别几把开多个实例。。。 -* **Browser Pool**: 浏览器页面提前开好,用完洗干净放回去 -* **Connection Pool**: Redis 和 HTTP 请求都用连接池 +所有全局资源通过单例管理器统一管理,避免重复创建和资源泄漏。 + +### Playwright 页面池 + +预初始化页面,无需每次都启动浏览器,大幅降低延迟。 + +### HTTP 连接复用 + +全局 aiohttp.ClientSession 支持 Keep-Alive,减少连接建立开销。 ## 4. 技术栈全景 diff --git a/docs/deployment.md b/docs/deployment.md index 62e8d2f..cc829bf 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,107 +1,240 @@ -# 部署指南 +# 生产环境部署 -把 Bot 扔到服务器上长期运行,比在自己电脑上跑要多几个步骤。 +将 NEO Bot 部署到服务器长期运行,只需要几个额外的步骤。本指南以 Linux 服务器为例。 ## 1. 环境准备 ### a. 安装 Python 3.14 -用3.14。。。 - -### b. 安装依赖 +在 Linux 服务器上安装 Python 3.14 及开发工具: ```bash -# 切换到项目目录 -cd /path/to/your/bot +# Ubuntu/Debian +sudo apt update +sudo apt install python3.14 python3.14-venv python3.14-dev gcc -# 创建虚拟环境 (强烈建议) +# CentOS/RHEL +sudo yum install python3.14 python3.14-devel gcc +``` + +### b. 克隆项目并创建虚拟环境 + +```bash +# 切换到项目目录(或新建) +cd /opt/neobot +git clone https://github.com/Fairy-Oracle-Sanctuary/NeoBot.git . + +# 创建虚拟环境(强烈建议) python3.14 -m venv venv source venv/bin/activate # 安装依赖 pip install -r requirements.txt +playwright install chromium ``` -### c. 编译核心模块 (可选,但为获得最佳性能强烈建议) +### c. 编译核心模块(可选但强烈推荐) -为了最大化性能,你可以将项目中的核心 Python 模块编译成 C 语言扩展。这将大幅提升机器人的响应速度和处理效率。 +为了最大化性能,建议在部署环境上编译 Mypyc 扩展: ```bash -# 确保你在虚拟环境中 -python setup_mypyc.py +# 确保已激活虚拟环境 +python setup_mypyc.py build_ext --inplace ``` -该脚本会自动编译 `core` 和 `models` 目录下的指定模块。编译后的文件(`.pyd` 或 `.so`)会直接生成在源码旁边。 +**注意**:编译产物是平台相关的,必须在目标服务器上执行。详见 [性能优化](../core-concepts/performance.md)。 -> **注意**: 编译产物是平台相关的(例如,在 Windows 上编译的 `.pyd` 文件不能在 Linux 上使用)。因此,**请务必在你最终部署的服务器环境(例如 Linux)上执行此编译步骤**。更多关于 Mypyc 编译的细节,请参考 [性能优化详解](core-concepts/performance.md)。 +## 2. 进程管理 -## 2. 使用进程管理器 +直接运行 `python main.py` 然后关闭 SSH 会导致 Bot 停止。需要用进程管理器来守护 Bot。 -你想直接 `python main.py` 然后关掉 SSH?那机器人也跟着停了。必须用进程管理器来守护它。 +推荐使用 `systemd`(Linux 原生方案)或 `pm2`。 -这里推荐用 `pm2`,虽然是 Node.js 的工具,但管 Python 程序一样好用。 +### 方案 A:systemd(推荐) -### a. 安装 pm2 +创建 `/etc/systemd/system/neobot.service` 文件: + +```ini +[Unit] +Description=NEO Bot Service +After=network.target redis.service + +[Service] +Type=simple +User=bot +WorkingDirectory=/opt/neobot +ExecStart=/opt/neobot/venv/bin/python -X jit main.py +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +Environment="PYTHONUNBUFFERED=1" + +[Install] +WantedBy=multi-user.target +``` + +然后启动服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable neobot +sudo systemctl start neobot + +# 查看状态 +sudo systemctl status neobot + +# 查看日志 +sudo journalctl -u neobot -f +``` + +### 方案 B:pm2 + +如果你习惯用 pm2(Node.js 工具),也可以: ```bash -# 你需要先装 Node.js 和 npm npm install pm2 -g ``` -### b. 启动 Bot - -在项目根目录,创建一个 `ecosystem.config.js` 文件: +创建 `ecosystem.config.js`: ```javascript module.exports = { apps : [{ name : "neobot", script : "main.py", - interpreter: "/path/to/your/bot/venv/bin/python", // 指定虚拟环境里的 python - max_memory_restart: "500M", // 内存超过 500M 自动重启 + interpreter: "/opt/neobot/venv/bin/python", + args: "-X jit", + max_memory_restart: "512M", env: { - "PYTHONUNBUFFERED": "1" // 禁用 python 输出缓冲,日志能实时看 - } + "PYTHONUNBUFFERED": "1" + }, + error_file: "./logs/pm2-error.log", + out_file: "./logs/pm2-out.log" }] } ``` -然后启动: +启动: ```bash pm2 start ecosystem.config.js +pm2 logs neobot +pm2 save +pm2 startup ``` -### c. 常用 pm2 命令 +## 3. 配置 OneBot 客户端 -```bash -pm2 list # 查看所有进程状态 -pm2 logs neobot # 查看 neobot 的实时日志 -pm2 restart neobot# 重启 neobot -pm2 stop neobot # 停止 neobot -pm2 delete neobot # 删除 neobot +Bot 使用 **正向 WebSocket 连接**,即 Bot 主动连接 OneBot 实现(如 NapCatQQ)。 + +在 `config.toml` 中配置: + +```toml +[napcat_ws] +# OneBot 客户端的 WebSocket 服务地址 +uri = "ws://127.0.0.1:3001" +token = "your_token_here" +reconnect_interval = 5 ``` -## 3. 配置 NapCatQQ +### NapCatQQ 配置示例 -最后一步,修改 NapCatQQ 的配置文件,让它把消息推送到你的服务器上。 - -找到 NapCatQQ 的 `config/onebot11.json` 文件,修改 `ws_reverse_servers` 部分: +在 NapCatQQ 的 `config/onebot11.json` 中,启用正向 WebSocket 服务器: ```json -"ws_reverse_servers": [ - { - "url": "ws://你的服务器IP:8080/onebot/v11/ws", - "access_token": "你的访问令牌" - } -] +{ + "ws": { + "enable": true, + "host": "127.0.0.1", + "port": 3001 + }, + "token": "your_token_here" +} ``` -* `url`: 改成你服务器的 IP 和 `main.py` 里配置的端口。 -* `access_token`: 如果你在 `main.py` 里设置了 `ACCESS_TOKEN`,这里要保持一致。 +然后重启 NapCatQQ,Bot 启动后应该能正常连接。 +## 4. 扩展配置 -或者你也可以用napcat的webui,不多赘述了。。。 +### Redis 连接 +确保 Redis 服务运行在可访问的地址,在 `config.toml` 配置: -改完后重启 NapCatQQ,Bot 应该就能收到消息了。 +```toml +[redis] +host = "127.0.0.1" +port = 6379 +db = 0 +password = "redis_password" # 如果有密码 +``` + +### Docker 代码沙箱(可选) + +若要使用 code_py 插件,需要配置 Docker: + +```toml +[docker] +base_url = "unix:///var/run/docker.sock" # Linux socket +sandbox_image = "python-sandbox:latest" +timeout = 10 +concurrency_limit = 5 +``` + +## 5. 监控和日志 + +### 查看日志 + +日志文件位于 `logs/` 目录,使用 `tail` 实时查看: + +```bash +tail -f logs/bot.log +``` + +### 监控系统资源 + +使用 systemd 时: + +```bash +# 查看内存和 CPU 使用 +systemctl status neobot +``` + +### 重启 Bot + +```bash +# systemd +sudo systemctl restart neobot + +# pm2 +pm2 restart neobot +``` + +## 6. 常见问题 + +### Redis 连接失败 + +检查 Redis 是否运行: + +```bash +redis-cli ping # 应返回 PONG +``` + +### Playwright 缓存问题 + +如果更新后图片渲染出现问题,清空 Playwright 缓存: + +```bash +rm -rf ~/.cache/ms-playwright +playwright install chromium +``` + +### 内存持续增长 + +检查是否有内存泄漏。在 systemd 中添加内存限制: + +```ini +[Service] +MemoryLimit=512M +MemoryAccounting=yes +``` diff --git a/docs/getting-started.md b/docs/getting-started.md index 7f30bde..ca2910b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,30 +1,24 @@ # 快速上手 -runit - ## 1. 你需要准备 -* **Python 3.14**: 必须是这个版本别问我为什么。。。 -* **Git**: 拉取代码 -* **Redis**: 装一个 -* **脑子和手**: 这个最重要,或者你去问问镀铬酸钾,会给你一对一教学的。。。 -* **OneBot v11 客户端**: 机器人本体,推荐用 [NapCatQQ](https://github.com/NapNeko/NapCatQQ) +* **Python 3.14**:必须是这个版本(JIT编译需要) +* **Git**:拉取代码 +* **Redis**:缓存和权限管理,需要单独安装 +* **Docker** (可选):用于代码沙箱执行(code_py插件) +* **OneBot v11 客户端**:机器人本体,推荐用 [NapCatQQ](https://github.com/NapNeko/NapCatQQ) -## 2. 搭起来 +## 2. 搭环境 ### a. 克隆代码 -找个你喜欢的地方,把代码从 GitHub 上clone下来 - ```bash -git clone [项目仓库地址] -cd [项目目录] +git clone https://github.com/Fairy-Oracle-Sanctuary/NeoBot.git +cd NeoBot ``` ### b. 创建虚拟环境 -别把你的系统环境搞得乱七八糟 - ```bash # Windows python -m venv venv @@ -39,17 +33,12 @@ source venv/bin/activate ### c. 安装依赖 - ```bash pip install -r requirements.txt ``` -这个文件里包含了所有需要的 Python 库,比如 `aiohttp` (HTTP 请求), `orjson` (JSON 解析), `jinja2` (模板渲染), `psutil` (系统监控) 等等。 - ### d. 安装 Playwright 依赖 -我们用 Playwright 来截图画画,它需要一个浏览器核心。 - ```bash playwright install chromium ``` diff --git a/docs/index.md b/docs/index.md index 4483aaf..7a27231 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,39 +1,38 @@ # NEO Bot 开发文档 -嘿,朋友,欢迎来到 NEO Bot +欢迎来到 NEO Bot Framework 开发文档! -这里没那么多规矩。这份文档是我写给你——未来的插件开发者、或者单纯好奇想拆开看看的家伙——的一份地图 +这是一个现代化的 Python QQ 机器人框架,基于 OneBot v11 协议,采用异步架构和性能优化技术。无论你是想快速搭建机器人,还是深入了解框架设计,这份文档都能帮助你。 -## 📖 地图导览 +## 📖 文档导览 -### 1. 准备阶段 -* [快速上手](./getting-started.md): 搭环境、装东西、启动。跟着走一遍,能省不少事。 -* [项目怎么样](./project-structure.md): 看看各个文件夹都是干嘛的,免得迷路。 -* [生产环境](./deployment.md): 怎么把你调教好的 Bot 扔服务器上,让它自己 7x24 小时跑。 +### 🚀 快速开始 +* [快速上手](./getting-started.md) - 5分钟搭建开发环境 +* [项目结构](./project-structure.md) - 了解代码组织方式 +* [生产部署](./deployment.md) - 将Bot部署到服务器 -### 2. 核心探秘 -* [骨架](./core-concepts/architecture.md): 看看镀铬酸钾和python打架,嗯。。。 -* [性能优化](./core-concepts/performance.md): 页面池、JIT、Mypyc... -* [消息流](./core-concepts/event-flow.md): 看看一条消息从被接收到被回复是如何运行的 -* [核心](./core-concepts/singleton-managers.md): `matcher`, `browser_manager`... 认识这些核心模块。 -* [Redis 原子操作与数据一致性](./core-concepts/redis-atomic-operations.md): 权限管理系统的原子操作实现,确保数据一致性 -* [错误处理](./core-concepts/error-handling.md): 了解系统的错误处理机制和错误码定义。 +### 💡 核心概念 +* [架构设计](./core-concepts/architecture.md) - 了解框架的设计理念 +* [性能优化](./core-concepts/performance.md) - JIT、Mypyc、页面池等优化技术 +* [事件流程](./core-concepts/event-flow.md) - 一条消息从接收到回复的完整流程 +* [核心管理器](./core-concepts/singleton-managers.md) - matcher、权限管理、浏览器池等 +* [Redis原子操作](./core-concepts/redis-atomic-operations.md) - 权限管理的分布式实现 +* [错误处理](./core-concepts/error-handling.md) - 异常处理和错误码体系 -### 3. API 参考 -* [API 总览](./api/index.md): 所有 API 的快速导航和调用方式 -* [消息 API](./api/message.md): 发消息、撤回、转发、合并转发 -* [群组 API](./api/group.md): 管群、禁言、踢人、改名片 -* [好友 API](./api/friend.md): 好友列表、点赞、加好友请求 -* [账号 API](./api/account.md): 机器人自己的信息、状态设置 -* [媒体 API](./api/media.md): 图片、语音相关 +### 🔌 API 参考 +* [API 总览](./api/index.md) - API 调用方式和快速导航 +* [消息 API](./api/message.md) - 发送、撤回、转发消息 +* [群组 API](./api/group.md) - 群管理、禁言、踢人等 +* [好友 API](./api/friend.md) - 好友列表、点赞等 +* [账号 API](./api/account.md) - 机器人自身信息获取 +* [媒体 API](./api/media.md) - 图片、语音、视频处理 -### 4. 插件开发 -* [插件开发第一步](./plugin-development/index.md): 带你写第一个插件 -* [指南](./plugin-development/command-handling.md): 怎么教你的 Bot 听懂指令和参数。 -* [绝对不要做的事情](./plugin-development/best-practices.md): **(必读!)** +### 📚 插件开发 +* [插件入门](./plugin-development/index.md) - 写你的第一个插件 +* [指令处理](./plugin-development/command-handling.md) - 参数解析、权限控制等 +* [最佳实践](./plugin-development/best-practices.md) - 避免常见的坑 +* [插件案例:状态监控](./plugin-development/status-plugin.md) - 深入学习复杂插件实现 -## 贡献 - -发现 Bug 了?觉得文档写得烂? -直接提 Issue 或者 PR。代码质量是第一位的,Talk is cheap, show me the code. +### 📋 开发规范 +* [开发规范](./development-standards.md) - 代码风格、异步编程、错误处理规范 diff --git a/docs/project-structure.md b/docs/project-structure.md index 75362d3..51a6da9 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -5,44 +5,136 @@ ``` . ├── core/ # 核心代码,别乱动 +│ ├── api/ # OneBot API 封装(消息、群组、好友、账号、媒体) │ ├── handlers/ # 底层事件处理器 │ ├── managers/ # 全局单例管理器 -│ ├── utils/ # 工具函数 -│ └── ws.py # WebSocket 连接实现 -├── data/ # 存放持久化数据 +│ │ ├── command_manager.py # 指令分发和事件处理 +│ │ ├── plugin_manager.py # 插件加载和热重载 +│ │ ├── permission_manager.py # 权限管理(Admin/User两级) +│ │ ├── browser_manager.py # Playwright页面池 +│ │ ├── image_manager.py # 图片/HTML模板渲染 +│ │ └── redis_manager.py # Redis缓存管理 +│ ├── utils/ # 工具函数和异常类 +│ │ ├── logger.py # 日志系统(Loguru) +│ │ ├── performance.py # 性能分析工具 +│ │ ├── executor.py # 代码沙箱执行引擎(Docker) +│ │ ├── exceptions.py # 自定义异常类 +│ │ └── singleton.py # 单例模式基类 +│ ├── ws.py # WebSocket 连接和消息处理(已Mypyc编译) +│ ├── bot.py # Bot 核心实例 +│ ├── config_loader.py # 配置文件加载 +│ ├── config_models.py # 配置数据模型 +│ └── permission.py # 权限枚举类 +│ +├── data/ # 持久化数据 │ ├── admin.json # 管理员列表 -│ └── permissions.json # 用户权限列表 -├── docs/ # 开发文档 -├── logs/ # 日志文件 +│ └── permissions.json # 用户权限配置 +│ ├── models/ # 数据模型 -│ └── events/ # OneBot 事件模型 -├── plugins/ # 你的插件都放这 -├── templates/ # 图片渲染用的网页模板 -├── venv/ # Python 虚拟环境 -├── .gitignore # Git 忽略配置 -├── main.py # 主入口文件 -├── requirements.txt # Python 依赖列表 -└── setup_mypyc.py # [可选] Mypyc 编译脚本,用于将核心模块编译为 C 扩展以提升性能 +│ ├── events/ # OneBot 11 事件模型 +│ │ ├── message.py # 消息事件 +│ │ ├── notice.py # 通知事件 +│ │ ├── request.py # 请求事件 +│ │ └── factory.py # 事件工厂 +│ ├── message.py # 消息段(CQ码) +│ ├── sender.py # 发送者信息 +│ └── objects.py # API响应对象(群信息、用户信息等) +│ +├── plugins/ # 你的插件都放这(最常修改的地方) +│ ├── admin.py # 权限管理(Admin/User两级权限) +│ ├── auto_approve.py # 自动同意好友请求和群邀请 +│ ├── bot_status.py # Bot运行状态查询(图片形式) +│ ├── broadcast.py # 管理员专用广播功能(隐藏插件) +│ ├── code_py.py # Python代码沙箱执行(多行输入、图片输出) +│ ├── echo.py # Echo和点赞功能 +│ ├── furry.py # Furry图片获取 +│ ├── github_parser.py # GitHub仓库链接自动解析 +│ ├── jrcd.py # 今日人品/长度查询(随机生成) +│ ├── thpic.py # 东方Project随机图片 +│ ├── web_parser/ # 综合Web链接解析系统 +│ │ ├── __init__.py # 主入口,自动检测链接 +│ │ ├── parsers/ # 各平台解析器 +│ │ │ ├── bili.py # B站视频/直播解析 +│ │ │ ├── douyin.py # 抖音视频解析 +│ │ │ └── github.py # GitHub仓库解析 +│ │ └── utils.py # 解析工具函数 +│ ├── sync_async_test_plugin.py # 异步同步混用测试(开发用) +│ └── resource/ # 插件资源文件 +│ +├── templates/ # Jinja2 HTML模板 +│ ├── code_execution.html # 代码执行结果展示 +│ ├── github_repo.html # GitHub仓库信息展示 +│ ├── help.html # 帮助页面 +│ └── status.html # Bot状态页面 +│ +├── web_static/ # 静态资源 +│ └── html/ # HTML资源文件 +│ +├── logs/ # 日志输出目录 +│ └── bot.log # 主日志文件 +│ +├── tests/ # 单元测试 +│ ├── test_api.py # API功能测试 +│ ├── test_bot.py # Bot核心测试 +│ ├── test_command_manager.py # 指令管理器测试 +│ ├── test_performance.py # 性能测试 +│ └── ... # 其他测试文件 +│ +├── docs/ # 开发文档 +│ ├── index.md # 文档首页 +│ ├── getting-started.md # 快速上手 +│ ├── project-structure.md # 项目结构(本文件) +│ ├── deployment.md # 生产环境部署 +│ ├── core-concepts/ # 核心概念详解 +│ ├── api/ # API参考文档 +│ └── plugin-development/ # 插件开发指南 +│ +├── scripts/ # 工具脚本 +│ ├── check_python_env.py # Python环境检查 +│ ├── compile_machine_code.py # 机器码编译 +│ └── export_requirements.py # 依赖导出 +│ +├── venv/ # Python 虚拟环境(git忽略) +├── __pycache__/ # Python缓存(git忽略) +├── .gitignore # Git忽略配置 +├── main.py # 启动入口 +├── config.toml # 配置文件(包含WS、Redis、Docker配置) +├── pytest.ini # 测试配置 +├── requirements.txt # Python依赖列表 +├── requirements-dev.txt # 开发依赖(包括pytest、mypy等) +├── setup_mypyc.py # Mypyc编译脚本(可选性能优化) +├── check_syntax.py # 语法检查脚本 +├── profile_main.py # 性能分析脚本 +├── test_performance_simple.py # 简单性能测试 +├── sandbox.Dockerfile # 代码沙箱Docker镜像 +├── LICENSE # 许可证 +└── README.md # 项目README ``` -## 重点目录说明 +## 核心目录说明 -### `core/` +### `core/` - 框架核心 +不用修改这里,除非你想优化框架本身。所有功能都由这里的管理器提供: +- **managers/** - 全局单例(matcher、permission_manager、browser_manager等) +- **api/** - OneBot API 封装 +- **handlers/** - 事件处理逻辑 -这是框架的心脏。除非你知道自己在干嘛,否则别碰这里面的东西。大部分功能都由 `managers` 里的管理器提供,你只需要 `import` 它们就行。 +### `plugins/` - 插件目录 +**这是你最常待的地方**。所有业务功能都在这里,包括现有的15+个插件。 -### `data/` +新建插件只需在这里添加 `.py` 文件,Bot 启动时会自动加载。支持热重载:修改后无需重启Bot。 -存放一些 JSON 格式的数据。管理员和用户权限默认存在这里。如果你用 Redis,这些文件会作为备份。 +### `data/` - 持久化数据 +- `admin.json` - 管理员QQ号列表 +- `permissions.json` - 用户权限配置 -### `plugins/` +这些文件也会自动同步到 Redis 以加快访问速度。 -**这是你最常待的地方**。你写的所有插件(`.py` 文件)都扔在这个目录里。Bot 启动时会自动加载这里的所有插件。 +### `templates/` - 图片模板 +使用 `ImageManager` 生成图片时,HTML模板放在这里。支持 Jinja2 模板语法。 -### `templates/` - -如果你要用 `ImageManager` 画图,就需要把 HTML 模板文件放在这里。 - -### `main.py` - -程序的入口。负责加载配置、初始化管理器、启动 WebSocket 连接和 FastAPI 服务。 +### `main.py` - 程序入口 +- 加载配置文件 +- 初始化各管理器和 WebSocket 连接 +- 启动插件加载器和文件监控(热重载) +- 处理程序生命周期 diff --git a/plugins/bot_status.py b/plugins/bot_status.py index cab92ed..523c068 100644 --- a/plugins/bot_status.py +++ b/plugins/bot_status.py @@ -7,7 +7,11 @@ import os import psutil import time import asyncio +import socket +import platform from datetime import datetime, timedelta +from functools import lru_cache +from typing import Optional from core.bot import Bot from core.managers.command_manager import matcher @@ -16,7 +20,7 @@ from core.managers.redis_manager import redis_manager from core.utils.executor import run_in_thread_pool from core.utils.logger import logger from models.events.message import MessageEvent, MessageSegment -from models.objects import LoginInfo, Status, VersionInfo +from models.objects import Status, VersionInfo __plugin_meta__ = { "name": "bot_status", @@ -28,14 +32,17 @@ __plugin_meta__ = { START_TIME = time.time() # 获取当前进程 PROCESS = psutil.Process(os.getpid()) +# 缓存bot昵称(12小时过期) +_nickname_cache: dict[str, tuple[str, float]] = {} def _get_system_info(): """ 同步函数:使用 psutil 获取系统信息,避免阻塞事件循环。 + 优化:使用 interval=None 获取自上次调用以来的平均 CPU 使用率 """ try: - # interval=1 会阻塞1秒,必须在线程池中运行 - cpu_percent = psutil.cpu_percent(interval=1) + # interval=None 会返回自上次调用以来的平均值,不会阻塞 + cpu_percent = psutil.cpu_percent(interval=None) mem_info = psutil.virtual_memory() bot_mem_mb = PROCESS.memory_info().rss / (1024 * 1024) @@ -45,159 +52,333 @@ def _get_system_info(): # 网络信息 net_io = psutil.net_io_counters() + # 进程数 + process_count = len(psutil.pids()) + + # CPU核心数 + cpu_count = psutil.cpu_count(logical=True) + cpu_count_physical = psutil.cpu_count(logical=False) + return { "cpu_percent": f"{cpu_percent:.1f}", + "cpu_count": cpu_count, + "cpu_count_physical": cpu_count_physical, "mem_percent": f"{mem_info.percent:.1f}", "mem_total": f"{mem_info.total / (1024**3):.1f}", "mem_used": f"{mem_info.used / (1024**3):.1f}", + "mem_available": f"{mem_info.available / (1024**3):.1f}", "bot_mem_mb": f"{bot_mem_mb:.2f}", "disk_percent": f"{disk_usage.percent:.1f}", "disk_total": f"{disk_usage.total / (1024**3):.1f}", "disk_used": f"{disk_usage.used / (1024**3):.1f}", + "disk_free": f"{disk_usage.free / (1024**3):.1f}", "net_sent": f"{net_io.bytes_sent / (1024**2):.1f}", "net_recv": f"{net_io.bytes_recv / (1024**2):.1f}", + "process_count": process_count, } except Exception as e: logger.error(f"获取系统信息失败: {e}") + return _create_error_system_info("N/A") + +async def _get_bot_nickname(bot: Bot) -> str: + """ + 异步获取bot昵称,带缓存机制(12小时过期) + """ + cache_key = f"bot_{bot.self_id}" + now = time.time() + + # 检查缓存是否有效 + if cache_key in _nickname_cache: + nickname, timestamp = _nickname_cache[cache_key] + if now - timestamp < 43200: # 12小时 + return nickname + + # 优先使用 get_stranger_info,更轻量 + try: + stranger_info = await bot.get_stranger_info(user_id=bot.self_id) + nickname = stranger_info.nickname + except Exception: + try: + login_info = await bot.get_login_info() + nickname = login_info.nickname + except Exception as e: + logger.warning(f"获取bot昵称失败: {e}") + nickname = "获取失败" + + _nickname_cache[cache_key] = (nickname, now) + return nickname + +async def _get_bot_info(bot: Bot, start_time: float) -> dict: + """ + 收集bot信息(id、昵称、头像、启动时间等) + """ + nickname = await _get_bot_nickname(bot) + + uptime_seconds = int(time.time() - start_time) + uptime_delta = timedelta(seconds=uptime_seconds) + days = uptime_delta.days + hours, remainder = divmod(uptime_delta.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}天 {hours:02}:{minutes:02}:{seconds:02}" + + return { + "user_id": bot.self_id, + "nickname": nickname, + "avatar_url": f"https://q1.qlogo.cn/g?b=qq&nk={bot.self_id}&s=640", + "start_time": datetime.fromtimestamp(start_time).strftime("%Y-%m-%d %H:%M:%S"), + "uptime": uptime_str, + } + +async def _get_version_info(bot: Bot) -> dict: + """ + 获取版本信息,失败时返回默认值 + """ + try: + version_info = await bot.get_version_info() return { - "cpu_percent": "N/A", - "mem_percent": "N/A", - "mem_total": "N/A", - "mem_used": "N/A", - "bot_mem_mb": "N/A", - "disk_percent": "N/A", - "disk_total": "N/A", - "disk_used": "N/A", - "net_sent": "N/A", - "net_recv": "N/A", + "app_name": version_info.app_name, + "app_version": version_info.app_version, + "protocol_version": version_info.protocol_version, + } + except Exception as e: + logger.warning(f"获取版本信息失败: {e}") + return { + "app_name": "获取失败", + "app_version": "N/A", + "protocol_version": "N/A", + } + +async def _get_stats(redis_manager) -> tuple[dict, list]: + """ + 获取统计数据和命令排行 + """ + try: + msgs_recv = await redis_manager.get("neobot:stats:messages_received") or 0 + msgs_sent = await redis_manager.get("neobot:stats:messages_sent") or 0 + command_stats_raw = await redis_manager.redis.hgetall("neobot:command_stats") + + total_commands = sum(int(v) for v in command_stats_raw.values()) if command_stats_raw else 0 + + stats_data = { + "messages_received": int(msgs_recv), + "messages_sent": int(msgs_sent), + "total_commands": total_commands, + } + + command_stats_data = sorted( + [{"name": k, "count": int(v)} for k, v in command_stats_raw.items()], + key=lambda x: x["count"], + reverse=True + ) if command_stats_raw else [] + + return stats_data, command_stats_data + except Exception as e: + logger.error(f"获取统计数据失败: {e}") + return { + "messages_received": 0, + "messages_sent": 0, + "total_commands": 0, + }, [] + +async def _get_system_info_async(timeout: float = 3.0) -> dict: + """ + 异步获取系统信息,带超时控制 + """ + try: + system_data = await asyncio.wait_for( + run_in_thread_pool(_get_system_info), + timeout=timeout + ) + return system_data + except asyncio.TimeoutError: + logger.error("获取系统信息超时") + return _create_error_system_info("Timeout") + except Exception as e: + logger.error(f"获取系统信息异常: {e}") + return _create_error_system_info("Error") + +async def _get_network_info_async() -> dict: + """ + 异步获取网络信息 + """ + try: + return await asyncio.wait_for( + run_in_thread_pool(_get_network_info), + timeout=2.0 + ) + except Exception as e: + logger.error(f"获取网络信息异常: {e}") + return { + "hostname": "获取失败", + "local_ip": "获取失败", + "public_ip": "获取失败", + } + +async def _get_os_info_async() -> dict: + """ + 异步获取操作系统信息 + """ + try: + return await asyncio.wait_for( + run_in_thread_pool(_get_os_info), + timeout=2.0 + ) + except Exception as e: + logger.error(f"获取操作系统信息异常: {e}") + return { + "os_name": "获取失败", + "os_version": "获取失败", + "os_arch": "获取失败", + "python_version": "获取失败", + } + +def _create_error_system_info(error_msg: str = "N/A") -> dict: + """ + 创建错误状态的系统信息字典 + """ + return { + "cpu_percent": error_msg, + "cpu_count": error_msg, + "cpu_count_physical": error_msg, + "mem_percent": error_msg, + "mem_total": error_msg, + "mem_used": error_msg, + "mem_available": error_msg, + "bot_mem_mb": error_msg, + "disk_percent": error_msg, + "disk_total": error_msg, + "disk_used": error_msg, + "disk_free": error_msg, + "net_sent": error_msg, + "net_recv": error_msg, + "process_count": error_msg, + } + +def _get_network_info(): + """ + 获取网络信息(IP地址、主机名等) + """ + try: + hostname = socket.gethostname() + + # 获取本地IP + try: + local_ip = socket.gethostbyname(hostname) + except: + local_ip = "获取失败" + + # 尝试获取公网IP(通过连接外部DNS) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + public_ip = s.getsockname()[0] + s.close() + except: + public_ip = "无法获取" + + return { + "hostname": hostname, + "local_ip": local_ip, + "public_ip": public_ip, + } + except Exception as e: + logger.error(f"获取网络信息失败: {e}") + return { + "hostname": "获取失败", + "local_ip": "获取失败", + "public_ip": "获取失败", + } + +def _get_os_info(): + """ + 获取操作系统信息 + """ + try: + os_name = platform.system() + os_version = platform.release() + os_arch = platform.machine() + python_version = platform.python_version() + + return { + "os_name": os_name, + "os_version": os_version, + "os_arch": os_arch, + "python_version": python_version, + } + except Exception as e: + logger.error(f"获取操作系统信息失败: {e}") + return { + "os_name": "获取失败", + "os_version": "获取失败", + "os_arch": "获取失败", + "python_version": "获取失败", } @matcher.command("status", "状态") async def handle_status(bot: Bot, event: MessageEvent, args: list[str]): """ 处理 status 指令,生成并回复机器人状态图片。 + 优化:并发获取各项数据,提升响应速度 """ logger.info(f"收到用户 {event.user_id} 的状态查询指令,开始生成状态图...") try: - # 1. 获取API信息 (增加独立错误处理) - # 尝试获取或更新 bot.nickname - if not hasattr(bot, "nickname") or not bot.nickname: - try: - # 优先使用 get_stranger_info 获取自身信息,比 get_login_info 更轻量 - stranger_info = await bot.get_stranger_info(user_id=bot.self_id) - bot.nickname = stranger_info.nickname - except Exception as e: - logger.warning(f"获取 stranger_info 失败: {e}, 将回退到 login_info") - try: - login_info = await bot.get_login_info() - bot.nickname = login_info.nickname - except Exception as e2: - logger.warning(f"获取 login_info 也失败了: {e2}") - bot.nickname = "获取失败" + # 并发获取所有数据,提升性能 + bot_info, version_info, stats_result, system_data, network_info, os_info = await asyncio.gather( + _get_bot_info(bot, START_TIME), + _get_version_info(bot), + _get_stats(redis_manager), + _get_system_info_async(timeout=3.0), + _get_network_info_async(), + _get_os_info_async(), + return_exceptions=False + ) - nickname = bot.nickname + # 处理 _get_stats 返回的元组 + if isinstance(stats_result, Exception): + logger.error(f"获取统计数据失败: {stats_result}") + stats_data, command_stats_data = {"messages_received": 0, "messages_sent": 0, "total_commands": 0}, [] + else: + stats_data, command_stats_data = stats_result + + # 处理异常返回值 + if isinstance(system_data, Exception): + logger.error(f"获取系统信息失败: {system_data}") + system_data = _create_error_system_info("Error") + + if isinstance(network_info, Exception): + logger.error(f"获取网络信息失败: {network_info}") + network_info = { + "hostname": "获取失败", + "local_ip": "获取失败", + "public_ip": "获取失败", + } + + if isinstance(os_info, Exception): + logger.error(f"获取操作系统信息失败: {os_info}") + os_info = { + "os_name": "获取失败", + "os_version": "获取失败", + "os_arch": "获取失败", + "python_version": "获取失败", + } - # 状态信息:如果能响应此命令,说明机器人必然在线且状态良好 - # 这避免了依赖可能超时或未实现的 get_status API - logger.debug("正在推断机器人状态...") + # 推断机器人状态(能响应此命令说明在线且状态良好) status_info = Status(online=True, good=True) - logger.debug(f"推断状态成功: online={status_info.online}, good={status_info.good}") - try: - version_info = await bot.get_version_info() - except Exception as e: - logger.warning(f"获取 version_info 失败: {e}") - version_info = VersionInfo(app_name="获取失败", app_version="N/A", protocol_version="N/A") - - # 2. 计算运行时长 - uptime_seconds = int(time.time() - START_TIME) - uptime_delta = timedelta(seconds=uptime_seconds) - days = uptime_delta.days - hours, remainder = divmod(uptime_delta.seconds, 3600) - minutes, seconds = divmod(remainder, 60) - uptime_str = f"{days}天 {hours:02}:{minutes:02}:{seconds:02}" - - bot_info_data = { - "user_id": bot.self_id, - "nickname": nickname, - "avatar_url": f"https://q1.qlogo.cn/g?b=qq&nk={bot.self_id}&s=640", - "start_time": datetime.fromtimestamp(START_TIME).strftime("%Y-%m-%d %H:%M:%S"), - "uptime": uptime_str, - } - - # 3. 获取统计数据 - try: - msgs_recv = await redis_manager.get("neobot:stats:messages_received") or 0 - msgs_sent = await redis_manager.get("neobot:stats:messages_sent") or 0 - command_stats_raw = await redis_manager.redis.hgetall("neobot:command_stats") - - total_commands = sum(int(v) for v in command_stats_raw.values()) - - stats_data = { - "messages_received": int(msgs_recv), - "messages_sent": int(msgs_sent), - "total_commands": total_commands, - } - - command_stats_data = sorted( - [{"name": k, "count": int(v)} for k, v in command_stats_raw.items()], - key=lambda x: x["count"], - reverse=True - ) - except Exception as e: - logger.error(f"获取Redis统计数据失败: {e}") - stats_data = { - "messages_received": 0, - "messages_sent": 0, - "total_commands": 0, - } - command_stats_data = [] - - # 4. 异步获取系统信息 - # 设置超时,防止 psutil 阻塞过久 - try: - system_data = await asyncio.wait_for(run_in_thread_pool(_get_system_info), timeout=5.0) - except asyncio.TimeoutError: - logger.error("获取系统信息超时") - system_data = { - "cpu_percent": "Timeout", - "mem_percent": "Timeout", - "mem_total": "Timeout", - "mem_used": "Timeout", - "bot_mem_mb": "Timeout", - "disk_percent": "Timeout", - "disk_total": "Timeout", - "disk_used": "Timeout", - "net_sent": "Timeout", - "net_recv": "Timeout", - } - except Exception as e: - logger.error(f"获取系统信息异常: {e}") - system_data = { - "cpu_percent": "Error", - "mem_percent": "Error", - "mem_total": "Error", - "mem_used": "Error", - "bot_mem_mb": "Error", - "disk_percent": "Error", - "disk_total": "Error", - "disk_used": "Error", - "net_sent": "Error", - "net_recv": "Error", - } - - # 5. 准备模板所需的所有数据 + # 准备模板数据 template_data = { - "bot_info": bot_info_data, + "bot_info": bot_info, "status_info": status_info, "version_info": version_info, "stats": stats_data, "system": system_data, + "network": network_info, + "os": os_info, "command_stats": command_stats_data, } - # 6. 渲染图片 + # 渲染图片 try: base64_str = await image_manager.render_template_to_base64( template_name="status.html", @@ -209,8 +390,7 @@ async def handle_status(bot: Bot, event: MessageEvent, args: list[str]): if base64_str: await event.reply(MessageSegment.image(base64_str)) else: - # 如果渲染失败,image_manager 内部会记录错误,这里给用户一个通用提示 - await event.reply("状态图片生成失败,可能是渲染服务出现问题,请联系管理员。") + await event.reply("状态图片生成失败,请稍后重试或联系管理员。") except Exception as e: logger.error(f"渲染图片失败: {e}") await event.reply("状态图片渲染过程中发生错误。") diff --git a/templates/status.html b/templates/status.html index af937c4..dc04bd8 100644 --- a/templates/status.html +++ b/templates/status.html @@ -141,6 +141,11 @@ column-count: 2; column-gap: 24px; } + .multi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 24px; + } .footer { margin-top: auto; padding: 32px 40px; @@ -221,10 +226,18 @@ CPU 占用 {{ system.cpu_percent }}% +
  • + CPU 核心 + {{ system.cpu_count }} ({{ system.cpu_count_physical }} 物理) +
  • 内存占用 {{ system.mem_percent }}% ({{ system.mem_used }}G / {{ system.mem_total }}G)
  • +
  • + 可用内存 + {{ system.mem_available }}G +
  • Bot 进程内存 {{ system.bot_mem_mb }} MB @@ -233,10 +246,18 @@ 磁盘占用 {{ system.disk_percent }}% ({{ system.disk_used }}G / {{ system.disk_total }}G)
  • +
  • + 磁盘可用 + {{ system.disk_free }}G +
  • 网络流量 (↑/↓) {{ system.net_sent }}MB / {{ system.net_recv }}MB
  • +
  • + 进程总数 + {{ system.process_count }} +
  • @@ -257,6 +278,46 @@ + +
    +

    网络信息 (Network)

    + +
    + +
    +

    操作系统 (OS)

    + +