拦截器
拦截器在消息到达命令插件之前和之后执行。适合用来做日志记录、频率限制、黑名单过滤等"横切"功能。
工作流程
一张图看懂拦截器的执行顺序:
收到消息
↓
[拦截器 A] pre_handle → true(放行)
↓
[拦截器 B] pre_handle → true(放行)
↓
[拦截器 C] pre_handle → false(拦截!停止!)
✗ 后续所有拦截器和插件都不会执行如果所有拦截器都放行:
[拦截器 A] pre_handle → true ✓
[拦截器 B] pre_handle → true ✓
↓
命令插件处理消息
↓
[拦截器 B] after_completion ← 逆序执行
[拦截器 A] after_completion关键规则
pre_handle按注册顺序执行(先 A 再 B)after_completion按逆序执行(先 B 再 A)pre_handle返回false→ 立即中止,后续拦截器和插件都不会执行after_completion总是执行,适合做清理工作
如何编写拦截器
第 1 步:实现 trait
rust
use qimen_plugin_api::prelude::*;
pub struct MyInterceptor;
#[async_trait]
impl MessageEventInterceptor for MyInterceptor {
/// 消息到达插件之前调用
/// 返回 true = 放行,false = 拦截
async fn pre_handle(&self, bot_id: &str, event: &NormalizedEvent) -> bool {
true // 默认放行
}
/// 所有插件处理完毕后调用(逆序)
/// 可以不写,默认为空实现
async fn after_completion(&self, bot_id: &str, event: &NormalizedEvent) {
// 清理、统计等
}
}第 2 步:注册到模块
在 #[module] 宏中通过 interceptors 属性注册:
rust
#[module(
id = "my-plugin",
interceptors = [LoggingInterceptor, CooldownInterceptor]
)]
#[commands]
impl MyPlugin {
// ...
}拦截器按列表顺序执行:先 LoggingInterceptor,后 CooldownInterceptor。
NormalizedEvent 便捷方法
在拦截器的 event 参数上,你可以调用这些方法获取信息:
| 方法 | 返回类型 | 说明 |
|---|---|---|
sender_id() | Option<&str> | 发送者 QQ 号 |
sender_id_i64() | Option<i64> | 发送者 QQ 号(数字) |
sender_nickname() | Option<&str> | 发送者昵称 |
sender_role() | Option<&str> | 群角色:"owner" / "admin" / "member" |
chat_id() | Option<&str> | 聊天 ID(群号或用户 ID) |
group_id() | Option<&str> | 群号(私聊为 None) |
is_group() | bool | 是否群聊 |
is_private() | bool | 是否私聊 |
plain_text() | String | 消息纯文本 |
message_id() | Option<i64> | 消息 ID |
is_at_self() | bool | 是否 @了 Bot |
is_group_admin_or_owner() | bool | 发送者是否为管理员或群主 |
实战示例
日志拦截器
记录每条消息的基本信息,便于调试和审计:
rust
pub struct LoggingInterceptor;
#[async_trait]
impl MessageEventInterceptor for LoggingInterceptor {
async fn pre_handle(&self, bot_id: &str, event: &NormalizedEvent) -> bool {
let sender = event.sender_id().unwrap_or("unknown");
let chat = event.chat_id().unwrap_or("unknown");
let text = event.plain_text();
let scope = if event.is_group() { "群聊" } else { "私聊" };
tracing::info!(
"[{bot_id}] {scope} | 发送者: {sender} | 会话: {chat} | 内容: {text}"
);
true // 始终放行,只记录日志
}
async fn after_completion(&self, bot_id: &str, event: &NormalizedEvent) {
let sender = event.sender_id().unwrap_or("unknown");
tracing::debug!("[{bot_id}] 消息处理完成: sender={sender}");
}
}冷却时间拦截器
防止用户刷屏,每个用户发消息后必须等待 3 秒:
rust
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub struct CooldownInterceptor {
last_message: Mutex<HashMap<String, Instant>>,
}
impl CooldownInterceptor {
pub fn new() -> Self {
Self {
last_message: Mutex::new(HashMap::new()),
}
}
}
#[async_trait]
impl MessageEventInterceptor for CooldownInterceptor {
async fn pre_handle(&self, _bot_id: &str, event: &NormalizedEvent) -> bool {
let sender = match event.sender_id() {
Some(id) => id.to_string(),
None => return true, // 无法识别发送者,放行
};
let cooldown = Duration::from_secs(3);
let now = Instant::now();
let mut map = self.last_message.lock().unwrap();
if let Some(last) = map.get(&sender) {
if now.duration_since(*last) < cooldown {
tracing::debug!("用户 {sender} 触发冷却限制,消息被拦截");
return false; // 拦截!
}
}
map.insert(sender, now);
true // 放行
}
}黑名单拦截器
禁止特定用户使用 Bot:
rust
pub struct BlacklistInterceptor {
blocked_users: Vec<i64>,
}
impl BlacklistInterceptor {
pub fn new(blocked_users: Vec<i64>) -> Self {
Self { blocked_users }
}
}
#[async_trait]
impl MessageEventInterceptor for BlacklistInterceptor {
async fn pre_handle(&self, _bot_id: &str, event: &NormalizedEvent) -> bool {
if let Some(sender_id) = event.sender_id_i64() {
if self.blocked_users.contains(&sender_id) {
tracing::info!("黑名单用户 {sender_id} 被拦截");
return false;
}
}
true
}
}关键词过滤拦截器
拦截包含违禁词的消息:
rust
pub struct KeywordFilterInterceptor {
forbidden_words: Vec<String>,
}
#[async_trait]
impl MessageEventInterceptor for KeywordFilterInterceptor {
async fn pre_handle(&self, _bot_id: &str, event: &NormalizedEvent) -> bool {
let text = event.plain_text().to_lowercase();
for word in &self.forbidden_words {
if text.contains(&word.to_lowercase()) {
tracing::info!("消息包含违禁词「{word}」,已拦截");
return false;
}
}
true
}
}动态插件中的拦截器
动态插件也可以注册拦截器。由于动态插件使用同步 extern "C" FFI,拦截器回调接收 InterceptorRequest 而不是 NormalizedEvent。
使用过程宏
rust
use abi_stable_host_api::{InterceptorRequest, InterceptorResponse};
#[dynamic_plugin(id = "my-plugin", version = "0.1.0")]
mod my_plugin {
use super::*;
#[pre_handle]
fn filter(req: &InterceptorRequest) -> InterceptorResponse {
let text = req.message_text.as_str();
if text.contains("spam") {
return InterceptorResponse::block();
}
InterceptorResponse::allow()
}
#[after_completion]
fn log(req: &InterceptorRequest) {
eprintln!("processed: sender={}", req.sender_id.as_str());
}
}InterceptorRequest 字段
| 字段 | 类型 | 说明 |
|---|---|---|
bot_id | RString | Bot 实例 ID |
sender_id | RString | 发送者 QQ 号 |
group_id | RString | 群号(私聊为空字符串) |
message_text | RString | 消息纯文本 |
raw_event_json | RString | 完整事件 JSON |
sender_nickname | RString | 发送者昵称 |
message_id | RString | 消息 ID |
timestamp | i64 | 事件 Unix 时间戳 |
InterceptorResponse
rust
InterceptorResponse::allow() // 放行
InterceptorResponse::block() // 拦截静态与动态拦截器共存
静态插件的拦截器和动态插件的拦截器会合并到同一条链中执行:先执行静态拦截器,再执行动态拦截器。热重载(/plugins reload)时动态拦截器会自动重建。
每模块限制
每个动态插件模块最多一个 #[pre_handle] 和一个 #[after_completion] 函数。
注意事项
拦截器对所有消息生效
拦截器对所有消息事件生效,不仅仅是命令消息。即使用户发的不是命令,pre_handle 和 after_completion 也会被调用。
在拦截器中要注意性能——每条消息都会经过所有拦截器。避免在拦截器中做耗时操作(如网络请求、数据库查询)。
状态管理
拦截器实例在整个运行期间保持存在。你可以使用 Mutex<HashMap<...>> 在拦截器中维护状态(如冷却计时器、计数器等)。但要注意线程安全——拦截器可能被并发调用。
