Skip to content

命令开发

命令是 QimenBot 最核心的交互方式。用户发送 /命令名 参数,Bot 通过命令处理函数做出响应。

最简命令

rust
#[command("回复 pong")]
async fn ping(&self) -> &str {
    "pong!"
}

用户发送 /ping,Bot 回复 pong!。就是这么简单。

命令名怎么来的?

如果你没有手动指定命令名,宏会自动从函数名推导

函数名推导出的命令名用户输入
pingping/ping
echoecho/echo
group_infogroup-info/group-info
my_cmdmy-cmd/my-cmd

规则很简单:下划线 _ 变连字符 -

你也可以手动指定:

rust
#[command(name = "greet", desc = "打招呼")]
async fn my_greeting_function(&self) -> &str {
    "你好!"
}
// 命令名是 "greet",不是 "my-greeting-function"

#[command] 完整属性

rust
#[command(
    "命令描述",                   // 必填,显示在 /help 中
    aliases = ["e", "回显"],      // 别名
    examples = ["/echo hello"],  // 使用示例
    category = "tools",          // 分类(默认 "general")
    role = "admin",              // 权限要求
    scope = "group",             // 作用域(默认 "all")
    hidden,                      // 不显示在 /help 中
)]
属性类型默认值说明
第一个参数&str必填命令描述(显示在 /help 中)
name&str函数名推导显式指定命令名
aliases[&str][]别名列表,用户可以用别名触发
examples[&str][]使用示例(显示在 /help 中)
category&str"general"命令分类
role&str无限制"admin"(管理员)或 "owner"(所有者)
scope&str"all"作用域:"all"(全部)、"group"(仅群聊)、"private"(仅私聊)
hiddenflag隐藏命令,不在 /help 中显示

别名示例

rust
#[command("回显文本", aliases = ["e", "回显"])]
async fn echo(&self, args: Vec<String>) -> String {
    args.join(" ")
}

现在 /echo hello/e hello/回显 hello 三种方式都能触发。

权限控制

rust
// 任何人都能用(默认)
#[command("回复 pong")]
async fn ping(&self) -> &str { "pong!" }

// 仅管理员可用
#[command("禁言用户", role = "admin")]
async fn ban(&self, ctx: &CommandPluginContext<'_>, args: Vec<String>) -> String {
    // ...
}

// 仅所有者可用
#[command("重载插件", role = "owner")]
async fn reload(&self) -> &str { "正在重载..." }

权限层级

角色配置来源权限范围
Ownerowners = ["QQ号"]所有命令
Adminadmins = ["QQ号"] 或群管理员role = "admin" 的命令
Anyone所有用户无权限限制的命令

作用域控制

通过 scope 属性声明命令的生效范围,框架在分发时自动过滤,不匹配的环境下命令静默忽略:

rust
// 默认:群聊 + 私聊都可用
#[command("回复 pong")]
async fn ping(&self) -> &str { "pong!" }

// 仅群聊可用(私聊中发送 /group-only 不会触发)
#[command("仅群聊命令", scope = "group")]
async fn group_only(&self) -> &str { "这是群聊命令" }

// 仅私聊可用
#[command("仅私聊命令", scope = "private")]
async fn private_only(&self) -> &str { "这是私聊命令" }
scope说明
"all" (默认)群聊和私聊均可触发
"group"仅在群聊中触发
"private"仅在私聊中触发

与手动检查的区别

使用 scope 属性后,不匹配的命令不会出现在 /help 列表中(按环境过滤),且分发层直接跳过,无需在回调函数内部手动判断 ctx.is_group()

方法签名

宏根据你的方法参数自动注入所需内容。共四种写法:

无参数 — 最简单

rust
#[command("回复 pong")]
async fn ping(&self) -> &str {
    "pong!"
}

只有 args — 获取用户输入

rust
#[command("回显文本")]
async fn echo(&self, args: Vec<String>) -> String {
    if args.is_empty() {
        "用法: /echo <文本>".to_string()
    } else {
        args.join(" ")
    }
}

args 是命令名后面的文本,按空格拆分:

/echo hello world → args = ["hello", "world"]
/echo             → args = []

只有 ctx — 获取上下文信息

rust
#[command("查看身份")]
async fn whoami(&self, ctx: &CommandPluginContext<'_>) -> String {
    let sender = ctx.sender_id();
    let scope = if ctx.is_group() { "群聊" } else { "私聊" };
    format!("你的 ID: {sender},当前环境: {scope}")
}

ctx + args — 两者都要

rust
#[command("禁言用户", role = "admin")]
async fn ban(&self, ctx: &CommandPluginContext<'_>, args: Vec<String>) -> CommandPluginSignal {
    let user_id = args.first().and_then(|s| s.parse::<i64>().ok());
    let duration = args.get(1).and_then(|s| s.parse::<i64>().ok()).unwrap_or(60);

    match (ctx.group_id_i64(), user_id) {
        (Some(gid), Some(uid)) => {
            let _ = ctx.onebot_actions().set_group_ban(gid, uid, duration).await;
            CommandPluginSignal::Reply(Message::text(format!("已禁言 {uid} {duration} 秒")))
        }
        _ => CommandPluginSignal::Reply(Message::text("用法: /ban <QQ号> [秒数]")),
    }
}

参数顺序

ctx 必须在 args 前面,不能反过来:

rust
// ✅ 正确
async fn cmd(&self, ctx: &CommandPluginContext<'_>, args: Vec<String>)

// ❌ 错误,编译不通过
async fn cmd(&self, args: Vec<String>, ctx: &CommandPluginContext<'_>)

CommandPluginContext 常用方法

ctx 是你和框架交互的入口,以下是最常用的方法:

发送者信息

方法返回类型说明
ctx.sender_id()&str发送者 QQ 号
ctx.sender_id_i64()Option<i64>发送者 QQ 号(数字)
ctx.event.sender_nickname()Option<&str>发送者昵称
ctx.event.sender_role()Option<&str>群角色:"owner" / "admin" / "member"
ctx.event.sender_card()Option<&str>群名片

聊天环境

方法返回类型说明
ctx.is_group()bool是否在群聊中
ctx.is_private()bool是否在私聊中
ctx.group_id()&str群号(私聊返回空字符串)
ctx.group_id_i64()Option<i64>群号(私聊返回 None)
ctx.chat_id()&str聊天 ID(群号或用户 ID)

消息内容

方法返回类型说明
ctx.plain_text()String消息纯文本(去掉图片、@等)
ctx.message()&Message完整消息对象(包含所有段)
ctx.event.message_id()Option<i64>消息 ID
ctx.event.is_at_self()bool用户是否 @了 Bot

调用 API

rust
let client = ctx.onebot_actions();
let info = client.get_login_info().await?;
let _ = client.send_group_msg(group_id, Message::text("hello")).await;

完整 API 列表见 OneBot API 客户端

返回值

命令处理函数支持多种返回值类型,框架自动帮你转换:

返回类型效果示例
&str发送文本回复"pong!"
String发送文本回复format!("hello {}", name)
Message发送富媒体回复Message::builder().text("hi").face(1).build()
CommandPluginSignal完全控制行为Reply / Continue / Block / Ignore
Result<T, E>Ok 正常处理,Err 发送错误信息Ok("done") / Err(e)"Error: ..."

CommandPluginSignal 详解

当你需要精确控制行为时,返回 CommandPluginSignal

信号效果
Reply(Message)发送回复消息,继续执行后续插件
Continue什么都不做,继续后续插件
Block(Message)发送回复消息,终止后续所有插件
Ignore什么都不做,终止后续所有插件

什么时候用 Block?

当你想"独占"处理这个命令时,用 Block 可以防止后续插件也响应同一个命令。比如一个验证码插件,验证成功后不希望其他插件再处理这条消息。

完整示例

rust
use qimen_plugin_api::prelude::*;

#[module(id = "example-basic", version = "0.1.0")]
#[commands]
impl BasicModule {
    /// 最简命令
    #[command("回复 pong")]
    async fn ping(&self) -> &str {
        "pong!"
    }

    /// 带参数和别名
    #[command("回显文本", aliases = ["e"])]
    async fn echo(&self, args: Vec<String>) -> String {
        if args.is_empty() {
            "用法: /echo <文本>".to_string()
        } else {
            args.join(" ")
        }
    }

    /// 使用上下文查询身份
    #[command("查看身份信息")]
    async fn whoami(&self, ctx: &CommandPluginContext<'_>) -> String {
        let sender = ctx.sender_id();
        let nickname = ctx.event.sender_nickname().unwrap_or("未知");
        let scope = if ctx.is_group() { "群聊" } else { "私聊" };
        format!("ID: {sender}\n昵称: {nickname}\n环境: {scope}")
    }

    /// 管理员命令:查询群信息
    #[command("查看群信息", aliases = ["gi"], role = "admin")]
    async fn group_info(&self, ctx: &CommandPluginContext<'_>) -> CommandPluginSignal {
        let Some(gid) = ctx.group_id_i64() else {
            return CommandPluginSignal::Reply(Message::text("此命令仅在群聊中可用"));
        };

        match ctx.onebot_actions().get_group_info(gid, false).await {
            Ok(info) => CommandPluginSignal::Reply(Message::text(format!(
                "群名: {}\n群号: {}\n成员数: {}",
                info.group_name, info.group_id, info.member_count.unwrap_or(0),
            ))),
            Err(e) => CommandPluginSignal::Reply(Message::text(format!("查询失败: {e}"))),
        }
    }

    /// 仅群聊命令
    #[command("仅群聊打招呼", scope = "group")]
    async fn group_only(&self, ctx: &CommandPluginContext<'_>) -> String {
        format!("群 {} 的朋友你好!", ctx.group_id())
    }

    /// 仅私聊命令
    #[command("仅私聊悄悄话", scope = "private")]
    async fn private_only(&self) -> &str {
        "这是一条只在私聊中可见的消息~"
    }

    /// 终止插件链
    #[command("阻止后续插件处理", hidden)]
    async fn stop(&self) -> CommandPluginSignal {
        CommandPluginSignal::Block(Message::text("插件链已终止"))
    }
}

基于 MIT 许可证发布