This commit is contained in:
2026-01-07 23:16:34 +08:00
3 changed files with 298 additions and 0 deletions

160
README.md
View File

@@ -13,7 +13,36 @@
本项目旨在提供一个稳定、高性能且开发体验优秀的机器人平台,服务于我们的社群管理和日常自动化需求。 本项目旨在提供一个稳定、高性能且开发体验优秀的机器人平台,服务于我们的社群管理和日常自动化需求。
### ✨ 核心特性 ### ✨ 核心特性
> **[INTERNAL USE ONLY]**
>
> 本仓库为 Calglau BOT 的内部开发版本,请遵守相关保密协议。
**Powered by NEO Bot Framework**
## 📖 项目概述
**Calglau BOT** 是一个基于 NEO Bot Framework 构建的、功能丰富的 QQ 机器人。它被设计为一个模块化、易于扩展的内部工具,通过插件化的方式集成了多种实用与娱乐功能。
本项目旨在提供一个稳定、高性能且开发体验优秀的机器人平台,服务于我们的社群管理和日常自动化需求。
### ✨ 核心特性
* **模块化插件架构**:所有功能均以独立插件形式存在于 `plugins/` 目录,易于开发、维护和热重载。
* **高性能异步核心**:基于 `asyncio``websockets`,确保在高并发消息下依然响应迅速。
* **开发者友好**:内置插件热重载,修改代码无需重启;完整的类型提示和清晰的 API 设计,提升开发效率。
* **集成 Redis 缓存**:自动缓存常用 API 调用(如群信息),减少重复请求,提升响应速度。
* **内置帮助系统**:通过 `/help` 指令可自动生成并展示所有已加载插件的功能说明。
### 🛠️ 技术栈
* **核心框架**: Python 3.8+ & NEO Bot Framework
* **异步库**: `asyncio`
* **网络通信**: `websockets` (OneBot v11)
* **缓存**: `Redis`
* **日志**: `Loguru`
* **文件监控**: `watchdog` (用于热重载)
---
* **模块化插件架构**:所有功能均以独立插件形式存在于 `plugins/` 目录,易于开发、维护和热重载。 * **模块化插件架构**:所有功能均以独立插件形式存在于 `plugins/` 目录,易于开发、维护和热重载。
* **高性能异步核心**:基于 `asyncio``websockets`,确保在高并发消息下依然响应迅速。 * **高性能异步核心**:基于 `asyncio``websockets`,确保在高并发消息下依然响应迅速。
* **开发者友好**:内置插件热重载,修改代码无需重启;完整的类型提示和清晰的 API 设计,提升开发效率。 * **开发者友好**:内置插件热重载,修改代码无需重启;完整的类型提示和清晰的 API 设计,提升开发效率。
@@ -52,10 +81,29 @@
│ ├── admin.json │ ├── admin.json
│ └── permissions.json │ └── permissions.json
├── html/ # 静态网页文件 ├── html/ # 静态网页文件
├── plugins/ # 插件目录,所有机器人的功能模块都在这里
│ ├── admin.py
│ ├── bili_parser.py
│ ├── code_py.py
│ ├── echo.py
│ ├── forward_test.py
│ ├── jrcd.py
│ └── thpic.py
├── core/ # NEO 框架核心代码,通常无需修改
│ ├── api/
│ ├── bot.py
│ ├── ...
│ └── ws.py
├── data/ # 数据存储目录 (管理员列表, 权限配置)
│ ├── admin.json
│ └── permissions.json
├── html/ # 静态网页文件
│ ├── 404.html │ ├── 404.html
│ └── index.html │ └── index.html
├── models/ # 数据模型 (事件, 消息段等) ├── models/ # 数据模型 (事件, 消息段等)
│ ├── ... │ ├── ...
├── models/ # 数据模型 (事件, 消息段等)
│ ├── ...
├── .gitignore ├── .gitignore
├── config.toml # 主配置文件 ├── config.toml # 主配置文件
├── main.py # 项目启动入口 ├── main.py # 项目启动入口
@@ -76,6 +124,11 @@
### 1. 环境准备 ### 1. 环境准备
* **Python 3.12 或更高版本**
* **我觉得**: 在开发和调试阶段使用官方的 **CPython** 解释器,以获得最佳的第三方库兼容性和调试体验。
* **你也可以觉得**: 在生产环境部署时,可以考虑使用 **PyPy** 以获取潜在的性能提升,但这可能会牺牲一定的兼容性。
* Redis 服务
* 一个正在运行的 OneBot v11 实现端 (推荐 **NapCatQQ**)
* **Python 3.12 或更高版本** * **Python 3.12 或更高版本**
* **我觉得**: 在开发和调试阶段使用官方的 **CPython** 解释器,以获得最佳的第三方库兼容性和调试体验。 * **我觉得**: 在开发和调试阶段使用官方的 **CPython** 解释器,以获得最佳的第三方库兼容性和调试体验。
* **你也可以觉得**: 在生产环境部署时,可以考虑使用 **PyPy** 以获取潜在的性能提升,但这可能会牺牲一定的兼容性。 * **你也可以觉得**: 在生产环境部署时,可以考虑使用 **PyPy** 以获取潜在的性能提升,但这可能会牺牲一定的兼容性。
@@ -84,6 +137,7 @@
### 2. 安装依赖 ### 2. 安装依赖
克隆本项目后,在项目根目录执行:
克隆本项目后,在项目根目录执行: 克隆本项目后,在项目根目录执行:
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
@@ -97,11 +151,22 @@ pip install -r requirements.txt
**因此,在拉取仓库后,您通常无需对 `config.toml` 文件进行任何修改即可直接运行。** **因此,在拉取仓库后,您通常无需对 `config.toml` 文件进行任何修改即可直接运行。**
如果您需要连接到本地或其他特定环境,可以参考以下配置结构进行修改。配置示例:
### 3. 配置
**[内部开发]**
为了方便内部开发和调试,项目中的 `config.toml` 文件已预先配置为连接到官方的 DEV 调试服务器。
**因此,在拉取仓库后,您通常无需对 `config.toml` 文件进行任何修改即可直接运行。**
如果您需要连接到本地或其他特定环境,可以参考以下配置结构进行修改。配置示例: 如果您需要连接到本地或其他特定环境,可以参考以下配置结构进行修改。配置示例:
```toml ```toml
# config.toml # config.toml
# config.toml
[napcat_ws] [napcat_ws]
# OneBot 实现端的 WebSocket 地址 # OneBot 实现端的 WebSocket 地址
uri = "ws://127.0.0.1:3001" uri = "ws://127.0.0.1:3001"
@@ -109,11 +174,26 @@ uri = "ws://127.0.0.1:3001"
token = "" token = ""
# 断线重连间隔(秒) # 断线重连间隔(秒)
reconnect_interval = 5 reconnect_interval = 5
# OneBot 实现端的 WebSocket 地址
uri = "ws://127.0.0.1:3001"
# Access Token (如果有)
token = ""
# 断线重连间隔(秒)
reconnect_interval = 5
[bot] [bot]
# 机器人指令的起始符号,可配置多个 # 机器人指令的起始符号,可配置多个
command_prefixes = ["/", "!", ""] command_prefixes = ["/", "!", ""]
[redis]
# Redis 连接信息
host = "127.0.0.1"
port = 6379
db = 0
password = ""
# 机器人指令的起始符号,可配置多个
command_prefixes = ["/", "!", ""]
[redis] [redis]
# Redis 连接信息 # Redis 连接信息
host = "127.0.0.1" host = "127.0.0.1"
@@ -141,7 +221,29 @@ Calglau BOT 的所有功能都通过插件实现。开发新功能非常简单
2.`plugins/` 目录下创建或修改任意 `.py` 文件。 2.`plugins/` 目录下创建或修改任意 `.py` 文件。
3. **保存文件** 3. **保存文件**
4. 观察控制台输出 `[HotReload] 插件重载完成` 的提示。你的新代码已即时生效。 4. 观察控制台输出 `[HotReload] 插件重载完成` 的提示。你的新代码已即时生效。
机器人启动后,将自动连接到 OneBot 实现端。控制台会输出加载的插件列表和连接状态。
---
## 🛠️ 插件开发指南
Calglau BOT 的所有功能都通过插件实现。开发新功能非常简单,并且得益于热重载,你无需在开发过程中频繁重启机器人。
### 🔥 热重载工作流
1. 保持 `python main.py` 进程运行。
2.`plugins/` 目录下创建或修改任意 `.py` 文件。
3. **保存文件**
4. 观察控制台输出 `[HotReload] 插件重载完成` 的提示。你的新代码已即时生效。
### 创建一个新插件
1.`plugins/` 目录下新建一个 Python 文件,例如 `weather.py`
2. 在该文件中编写你的逻辑。
#### 1. 定义插件元数据 (`__plugin_meta__`)
为了让 `/help` 指令能自动发现你的插件,请在文件顶部定义 `__plugin_meta__` 字典:
### 创建一个新插件 ### 创建一个新插件
1.`plugins/` 目录下新建一个 Python 文件,例如 `weather.py` 1.`plugins/` 目录下新建一个 Python 文件,例如 `weather.py`
@@ -153,6 +255,7 @@ Calglau BOT 的所有功能都通过插件实现。开发新功能非常简单
```python ```python
# plugins/weather.py # plugins/weather.py
# plugins/weather.py
__plugin_meta__ = { __plugin_meta__ = {
"name": "天气查询", "name": "天气查询",
@@ -163,23 +266,39 @@ __plugin_meta__ = {
#### 2. 编写指令处理器 #### 2. 编写指令处理器
使用 `@matcher.command()` 装饰器来注册一个聊天指令。
"name": "天气查询",
"description": "提供城市天气查询功能。",
"usage": "/weather [城市名] - 查询指定城市的实时天气。",
}
```
#### 2. 编写指令处理器
使用 `@matcher.command()` 装饰器来注册一个聊天指令。 使用 `@matcher.command()` 装饰器来注册一个聊天指令。
```python ```python
# plugins/weather.py # plugins/weather.py
# plugins/weather.py
from core.command_manager import matcher from core.command_manager import matcher
from models import MessageEvent from models import MessageEvent
# ... (元数据定义) ...
# ... (元数据定义) ... # ... (元数据定义) ...
@matcher.command("weather") @matcher.command("weather")
async def handle_weather_command(event: MessageEvent, args: list[str]):
async def handle_weather_command(event: MessageEvent, args: list[str]): async def handle_weather_command(event: MessageEvent, args: list[str]):
""" """
处理 /weather 指令 处理 /weather 指令
:param event: 消息事件对象,用于回复等操作 :param event: 消息事件对象,用于回复等操作
:param args: 用户发送的参数列表 (已按空格分割) :param args: 用户发送的参数列表 (已按空格分割)
处理 /weather 指令
:param event: 消息事件对象,用于回复等操作
:param args: 用户发送的参数列表 (已按空格分割)
""" """
if not args: if not args:
await event.reply("请输入要查询的城市名,例如:/weather 北京")
await event.reply("请输入要查询的城市名,例如:/weather 北京") await event.reply("请输入要查询的城市名,例如:/weather 北京")
return return
@@ -189,19 +308,35 @@ async def handle_weather_command(event: MessageEvent, args: list[str]):
# (示例代码,省略了真实 API 调用) # (示例代码,省略了真实 API 调用)
weather_data = f"{city}的天气是25°C。" weather_data = f"{city}的天气是25°C。"
await event.reply(weather_data)
city = args[0]
# 此处应调用天气 API 获取数据
# (示例代码,省略了真实 API 调用)
weather_data = f"{city}的天气是25°C。"
await event.reply(weather_data) await event.reply(weather_data)
``` ```
#### 3. 监听事件 #### 3. 监听事件
除了指令,你还可以监听各种事件,例如新成员入群。
#### 3. 监听事件
除了指令,你还可以监听各种事件,例如新成员入群。 除了指令,你还可以监听各种事件,例如新成员入群。
```python ```python
from core.command_manager import matcher from core.command_manager import matcher
from models import GroupIncreaseNoticeEvent from models import GroupIncreaseNoticeEvent
from models import GroupIncreaseNoticeEvent
from core.bot import Bot from core.bot import Bot
@matcher.on_notice("group_increase") @matcher.on_notice("group_increase")
async def welcome_new_member(bot: Bot, event: GroupIncreaseNoticeEvent):
"""当有新成员加入群聊时触发"""
welcome_message = f"欢迎新成员 @{event.user_id} 加入本群!"
await bot.send_group_msg(event.group_id, welcome_message)
async def welcome_new_member(bot: Bot, event: GroupIncreaseNoticeEvent): async def welcome_new_member(bot: Bot, event: GroupIncreaseNoticeEvent):
"""当有新成员加入群聊时触发""" """当有新成员加入群聊时触发"""
welcome_message = f"欢迎新成员 @{event.user_id} 加入本群!" welcome_message = f"欢迎新成员 @{event.user_id} 加入本群!"
@@ -226,6 +361,31 @@ async def welcome_new_member(bot: Bot, event: GroupIncreaseNoticeEvent):
## 🗺️ 路线图 (Roadmap) ## 🗺️ 路线图 (Roadmap)
- [ ] **Web 仪表盘**: 开发一个简单的 Web 页面,用于查看机器人状态和插件列表。
- [ ] **权限系统重构**: 引入更精细化的权限节点,允许按插件或指令控制用户权限。
- [ ] **数据库集成**: 引入 `SQLite` 或其他数据库,用于需要持久化存储数据的功能。
- [ ] **新插件开发**:
- [ ] 天气查询插件
- [ ] GIL实现
- [ ] coming soon...
---
## 📦 当前功能插件
| 插件文件 (`plugins/`) | 功能描述 |
|-----------------------|----------|
| `admin.py` | 机器人管理员权限管理 |
| `bili_parser.py` | 自动解析 Bilibili 视频链接分享卡片 |
| `code_py.py` | 执行 Python 代码片段 (高危,仅限管理员) |
| `echo.py` | 提供 `/echo` 复读和 `/赞我` 功能 |
| `forward_test.py` | 演示如何发送合并转发消息 |
| `jrcd.py` | 娱乐功能:今日人品、牛牛词典 |
| `thpic.py` | 发送一张随机的东方 Project 图片 |
---
## 🗺️ 路线图 (Roadmap)
- [ ] **Web 仪表盘**: 开发一个简单的 Web 页面,用于查看机器人状态和插件列表。 - [ ] **Web 仪表盘**: 开发一个简单的 Web 页面,用于查看机器人状态和插件列表。
- [ ] **权限系统重构**: 引入更精细化的权限节点,允许按插件或指令控制用户权限。 - [ ] **权限系统重构**: 引入更精细化的权限节点,允许按插件或指令控制用户权限。
- [ ] **数据库集成**: 引入 `SQLite` 或其他数据库,用于需要持久化存储数据的功能。 - [ ] **数据库集成**: 引入 `SQLite` 或其他数据库,用于需要持久化存储数据的功能。

View File

@@ -82,6 +82,7 @@ class MessageHandler(BaseHandler):
def command( def command(
self, self,
*names: str, *names: str,
*names: str,
permission: Optional[Permission] = None, permission: Optional[Permission] = None,
override_permission_check: bool = False override_permission_check: bool = False
) -> Callable: ) -> Callable:

View File

@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import html import html
import textwrap import textwrap
# -*- coding: utf-8 -*-
import html
import textwrap
import asyncio import asyncio
from typing import Dict from typing import Dict
@@ -13,12 +16,20 @@ __plugin_meta__ = {
"name": "Python 代码执行", "name": "Python 代码执行",
"description": "在安全的沙箱环境中执行 Python 代码片段,支持单行、多行和转发回复。", "description": "在安全的沙箱环境中执行 Python 代码片段,支持单行、多行和转发回复。",
"usage": "/py <单行代码>\n/code_py <单行代码>\n/py (进入多行输入模式)", "usage": "/py <单行代码>\n/code_py <单行代码>\n/py (进入多行输入模式)",
"name": "Python 代码执行",
"description": "在安全的沙箱环境中执行 Python 代码片段,支持单行、多行和转发回复。",
"usage": "/py <单行代码>\n/code_py <单行代码>\n/py (进入多行输入模式)",
} }
# --- 会话状态管理 --- # --- 会话状态管理 ---
# 结构: {(user_id, group_id): asyncio.TimerHandle} # 结构: {(user_id, group_id): asyncio.TimerHandle}
multi_line_sessions: Dict[tuple, asyncio.TimerHandle] = {} multi_line_sessions: Dict[tuple, asyncio.TimerHandle] = {}
async def reply_as_forward(event: MessageEvent, input_code: str, output_result: str):
# --- 会话状态管理 ---
# 结构: {(user_id, group_id): asyncio.TimerHandle}
multi_line_sessions: Dict[tuple, asyncio.TimerHandle] = {}
async def reply_as_forward(event: MessageEvent, input_code: str, output_result: str): async def reply_as_forward(event: MessageEvent, input_code: str, output_result: str):
""" """
将输入和输出打包成转发消息进行回复。 将输入和输出打包成转发消息进行回复。
@@ -48,9 +59,38 @@ async def reply_as_forward(event: MessageEvent, input_code: str, output_result:
# 降级为普通消息回复 # 降级为普通消息回复
await event.reply(f"--- 你的代码 ---\n{input_code}\n--- 执行结果 ---\n{output_result}") await event.reply(f"--- 你的代码 ---\n{input_code}\n--- 执行结果 ---\n{output_result}")
async def execute_code(event: MessageEvent, code: str):
将输入和输出打包成转发消息进行回复
参考 forward_test.py 的实现兼容私聊和群聊
"""
bot = event.bot
# 1. 构建消息节点列表
nodes = [
bot.build_forward_node(
user_id=event.user_id,
nickname=event.sender.nickname or str(event.user_id),
message=f"--- Your Code ---\n{input_code}"
),
bot.build_forward_node(
user_id=event.self_id,
nickname="Code Executor",
message=f"--- Execution Result ---\n{output_result}"
)
]
try:
# 2. 发送合并转发消息
await bot.send_forwarded_messages(event, nodes)
except Exception as e:
logger.error(f"[code_py] 发送转发消息失败: {e}")
# 降级为普通消息回复
await event.reply(f"--- 你的代码 ---\n{input_code}\n--- 执行结果 ---\n{output_result}")
async def execute_code(event: MessageEvent, code: str): async def execute_code(event: MessageEvent, code: str):
""" """
核心代码执行逻辑 核心代码执行逻辑
核心代码执行逻辑
""" """
code_executor = getattr(event.bot, 'code_executor', None) code_executor = getattr(event.bot, 'code_executor', None)
if not code_executor or not code_executor.docker_client: if not code_executor or not code_executor.docker_client:
@@ -97,15 +137,74 @@ def normalize_code(code: str) -> str:
return code.strip() return code.strip()
@matcher.command("py", "python", "code_py", permission=ADMIN)
async def code_py_main(event: MessageEvent, args: list[str]):
code_executor = getattr(event.bot, 'code_executor', None)
if not code_executor or not code_executor.docker_client:
await event.reply("代码执行服务当前不可用,请检查 Docker 连接配置。")
return
# 修改 add_task让它能直接接收回复函数
await code_executor.add_task(
code,
lambda result: reply_as_forward(event, code, result)
)
await event.reply("代码已提交至沙箱执行队列,请稍候...")
def cleanup_session(session_key: tuple):
"""
清理超时的会话
"""
if session_key in multi_line_sessions:
del multi_line_sessions[session_key]
logger.info(f"[code_py] 会话 {session_key} 已超时,自动取消。")
def normalize_code(code: str) -> str:
"""
规范化用户输入的 Python 代码字符串
主要处理两个问题
1. 对消息中可能存在的 HTML 实体进行解码 (e.g., &#91; -> [)。
2. 移除整个代码块的公共前导缩进以修复因复制粘贴导致的多余缩进
:param code: 原始代码字符串
:return: 规范化后的代码字符串
"""
# 1. 解码 HTML 实体
code = html.unescape(code)
# 2. 移除公共前导缩进
try:
code = textwrap.dedent(code)
except Exception:
# 在某些情况下例如不一致的缩进dedent 可能会失败,
# 但我们不希望因此中断流程,所以捕获异常并继续。
pass
return code.strip()
@matcher.command("py", "python", "code_py", permission=ADMIN) @matcher.command("py", "python", "code_py", permission=ADMIN)
async def code_py_main(event: MessageEvent, args: list[str]): async def code_py_main(event: MessageEvent, args: list[str]):
""" """
/py 命令的主入口 /py 命令的主入口
- 如果有参数直接执行 - 如果有参数直接执行
- 如果没有参数开启多行输入模式 - 如果没有参数开启多行输入模式
/py 命令的主入口
- 如果有参数直接执行
- 如果没有参数开启多行输入模式
""" """
code_to_run = " ".join(args) code_to_run = " ".join(args)
if code_to_run:
# 单行模式,对代码进行规范化处理
normalized_code = normalize_code(code_to_run)
if not normalized_code:
await event.reply("代码为空或格式错误,请输入有效的代码。")
return
await execute_code(event, normalized_code)
code_to_run = " ".join(args)
if code_to_run: if code_to_run:
# 单行模式,对代码进行规范化处理 # 单行模式,对代码进行规范化处理
normalized_code = normalize_code(code_to_run) normalized_code = normalize_code(code_to_run)
@@ -132,6 +231,24 @@ async def code_py_main(event: MessageEvent, args: list[str]):
session_key session_key
) )
multi_line_sessions[session_key] = timeout_handler multi_line_sessions[session_key] = timeout_handler
# 多行模式
# 使用 getattr 兼容私聊和群聊
session_key = (event.user_id, getattr(event, 'group_id', 'private'))
# 如果上一个会话的超时任务还在,先取消它
if session_key in multi_line_sessions:
multi_line_sessions[session_key].cancel()
await event.reply("已进入多行代码输入模式,请直接发送你的代码。\n(60秒内无操作将自动取消)")
# 设置 60 秒超时
loop = asyncio.get_running_loop()
timeout_handler = loop.call_later(
60,
cleanup_session,
session_key
)
multi_line_sessions[session_key] = timeout_handler
@matcher.on_message() @matcher.on_message()
async def handle_multi_line_code(event: MessageEvent): async def handle_multi_line_code(event: MessageEvent):
@@ -148,6 +265,26 @@ async def handle_multi_line_code(event: MessageEvent):
# 对多行代码进行规范化处理 # 对多行代码进行规范化处理
normalized_code = normalize_code(event.raw_message) normalized_code = normalize_code(event.raw_message)
if not normalized_code:
await event.reply("捕获到的代码为空或格式错误,已取消输入。")
return
await execute_code(event, normalized_code)
return True # 消费事件,防止其他处理器响应
async def handle_multi_line_code(event: MessageEvent):
"""
通用消息处理器用于捕获多行模式下的代码输入
"""
# 使用 getattr 兼容私聊和群聊
session_key = (event.user_id, getattr(event, 'group_id', 'private'))
if session_key in multi_line_sessions:
# 取消超时任务
multi_line_sessions[session_key].cancel()
del multi_line_sessions[session_key]
# 对多行代码进行规范化处理
normalized_code = normalize_code(event.raw_message)
if not normalized_code: if not normalized_code:
await event.reply("捕获到的代码为空或格式错误,已取消输入。") await event.reply("捕获到的代码为空或格式错误,已取消输入。")
return return