快速上手 · 安装 · 协议与传输 · 示例 · 设计要点 · docs.rs ↗
nagisa 是 Rust QQ bot 框架库,把 OneBot v11 与 Milky 两套协议统一到一套强类型 API 之下:业务代码只看见 Event / Message / Segment / Peer / Uin / MessageId,不碰协议 JSON 与 retcode,切换协议端零业务改动。协议端(Lagrange / NapCat / LLOneBot 等真正登录 QQ 的外部程序)需自行部署,nagisa 负责连上它。
flowchart LR
P["协议端<br/>Lagrange · NapCat · LLOneBot"]
subgraph nagisa
direction LR
A["适配器<br/>onebot · milky"]
E["统一域模型<br/>Event · Segment · Peer"]
R["Router 三层分发<br/>门控 · 冷却 · 中间件"]
H["你的 handler"]
A -->|解码| E --> R --> H
H -->|"Reply · bot 动作"| A
end
P <-->|"WS · HTTP · SSE"| A
安装
[]
= "0.2" # 默认含 onebot + milky 两套适配器
# nagisa = { version = "0.2", features = ["log"] } # 额外开事件日志(控制台 / 文件渲染)
或一行 cargo add nagisa。另需一个已登录 QQ 的协议端(Lagrange / NapCat / LLOneBot 等)在外部运行,Nagisa 负责连上它。
快速上手
use *;
// 「@bot echo 你好」 → 「echo: 你好」
async
async
- 换协议:
run_onebot(OneBotConfig::new(..))→run_milky(MilkyConfig::new(..)),handler 一字不改。 - 前提:一个已登录 QQ 的协议端(见 安装)。
use nagisa::prelude::*;是业务侧唯一需要的导入,不直接引用nagisa-core/nagisa-types等底层 crate。
命令
async
#[command(..)] 属性键:
| 类 | 键 |
|---|---|
| 匹配器(三选一,必给) | 前导位置参数 "a", "b" 字面量命令词(写在最前);regex = "^..$" 原始正则;slots = Type 用 #[derive(Slots)] 类型作头 |
| 行为 | mention_me 要求 @bot;top 置顶观察者(恒跑,不被 waiter 拦截);priority = N 越大越先;usage = ".." 参数解析失败时回贴的用法串(不配则静默跳过) |
| 门控 | gate = <Rule 表达式>;cooldown = <秒数或 Cooldown 构建体>(恒在门控链最右:权限先判,通过才盖戳) |
| 元数据 | id / name(缺省取函数名)、description、can_disable(默认 true)、default_enable(默认 true)、hidden(默认 false) |
命令头写在属性里,参数声明在 handler 形参上(Args<T> / ArgText 等),就近落在同一个函数。
参数
#[arg(..)] 字段属性:
| 形态 | 写法 |
|---|---|
| 文本位置参数 | 默认(无 #[arg]);Option<T> 可选;#[arg(default = "..")] 缺省值 |
| 剩余文本 | #[arg(rest)];#[arg(rest, raw)] 保真原文空白、旗标只认前导 |
| 选项 / 旗标 | #[arg(long)](可 long = "name"、short = 'c');#[arg(flag)] 布尔旗标 |
| 消息元素 | #[arg(image | record | video | at | reply | face)] 按类型从消息段里取;Option<T> 可选;加 rest 收全部(Vec<Media>) |
| @ 或裸号 | #[arg(at_or_id)] u: Uin:取 @ 元素,缺则取下一个文本词当 QQ 号(群 @、私聊输号一字段通吃) |
文本字段类型需实现 FromArg(String / 整数 / f64 / bool / Uin 内置);元素字段类型固定:image/record/video → Media、at → Uin、reply → MessageId、face → String。受限选项用 #[derive(ArgEnum)](变体名小写匹配,#[arg(rename = "原曲", alias = "origin")] 改名/加别名)。
另一套头匹配:#[derive(Slots)] 把「命令头 + 命名正则槽」结构体化——#[slots(full = "签到", usage = "..")] + 字段上 #[slot(re = r"(\d+)")] / #[slot(union = ["a", "b"])] / #[slot(tail)],同一类型既当 #[command(slots = T)] 的头又当 Slots<T> 提取器;matcher!{} 是它的内联纯匹配器糖。
事件
async
#[event(<Kind>, ..)] 第一个位置参数是 EventKind 变体,其余键同 #[command](top / priority / gate / cooldown / 元数据;无匹配器,usage 被显式拒绝)。可用 Kind:
- 消息:
Message - 群通知:
MemberJoinMemberLeaveMuteWholeMuteAdminChangeGroupNameChangeGroupCardChangeGroupTitleChangeGroupDismissHonorLuckyKingEssenceChangeRecallReactionGroupFileUpload - 好友/杂项通知:
FriendAddFriendFileUploadNudgePokeRecallPeerPinInputStatusProfileLikeGrayTipOnlineFileFlashFile - 请求:
FriendRequestGroupJoinRequestGroupInvitedJoinGroupInvite - 生命周期:
ConnectDisconnectReady(self_id 解析完成,启动逻辑写在这)HeartbeatBotOnlineBotOffline - 兜底:
Raw
插件与开关
plugin!
同模块(及子模块)里的 #[command] / #[event] 按 module_path!() 最长前缀自动归属本插件,App::new() 经 inventory 一次性收集,main 里零登记代码。不在任何 plugin!{} 模块下的裸 #[command] 会落进按 module_path 合成的隐式插件——没有像样的身份和开关,别这么写。开关分两层:插件总开关 + 每触发器子开关(echo / echo.transfer),按会话覆盖 > 全局 > 默认;registered_plugins() / registered_triggers() 可枚举注册表。
注册可见性:
inventory靠链接器收集注册项。插件 crate 若没被 binary 源码引用,会被链接器整体丢弃——命令静默消失,零报错。务必use my_plugins::*;或调一次force_link(),详见App::new文档。
门控与冷却
内置 Rule,用 & | ! 组合(左短路):
| 类 | 规则 |
|---|---|
| 场景 | to_me()(私聊或群里 @bot)、group_only()、private()、in_group(g)、from_user(u) |
| 内容 | keyword(["词", ..]) |
| 权限 | superuser()(名单经 App::superusers([..]) 配置)、group_admin()、group_owner()——群角色查询结果在本事件内 memo,组合也只发一次请求 |
| 全局闸 | switch(key) 运行期开关、awake() 休眠时否决并回贴、awake_silent() 静默版 |
| 包装 | replying(rule, on_veto) 把任意规则变成「否决时回话」 |
| 自定义 | Rule::pred(|ctx| ..) 同步谓词、Rule::new(..) 异步 |
注意 group_admin() / group_owner() 在私聊场景恒为否(fail-closed),跨场景管理命令写 gate = group_admin() | superuser()。
冷却三种用法:cooldown = 30(每用户全局桶);cooldown = Cooldown::new(30).per_trigger().max_exec(3).bypass(superuser())(滑动窗口多次额度,.per_peer() / .global() 改作用域);数据派生的条件冷却用 Cd 提取器命令式控制——cd.gate(key, dur)? 检查并盖章、cd.ready(key, dur) 只看不盖、cd.stamp(key, dur) 成功后才盖。全局限流用 RateLimit 中间件(App::layer)。
提取器
handler 形参按类型注入,类型提不出来时该 handler 自动跳过;Option<T> 把任一项变成可选。
| 组 | 类型 |
|---|---|
| 事件 | Arc<Event>、MessageEvent、Notice、Request、Meta、Scene |
| 场景过滤 | GroupMessage、PrivateMessage、Sender(Uin)、EventPeer(Peer)、ToMe(bool) |
| 命令(需匹配器命中) | Command(命中词)、ArgText(剩余文本)、CommandArg(剩余段)、Captures(正则捕获)、Args<T>、Slots<T> |
| 消息元素 | At(首个 @)、Image(首个图片;无命令上下文时在整条消息里找) |
| 设施 | Bot、Reply、State<T>(App::data 注入的共享态)、Session(多轮会话)、Cd(冷却句柄) |
| 事件专属 | MemberJoin / Nudge / Recall / Ready … 与 #[event(Kind)] 同名的结构化载荷 |
自定义提取器:给类型实现 FromContext(impl 块标 #[nagisa::async_trait])。
多轮会话
async
recv::<T>()可等任意提取器类型——等图片就recv::<Image>(),和读内联参数#[arg(image)]得到的是同一个Media,「内联给参或事后追问」一处分支搞定。- 高层封装:
confirm(d, 谁, "是", "否")(只收指定用户的回复,认不出的输入自动追问)、recv_text(d, 取消词)、recv_parse(d, 取消词, 解析fn)——解析失败自动回贴追问。 - 嵌套会话深者优先,guard Drop 自动摘除;命中的 waiter 默认吞掉该事件(
top观察者不受影响,恒跑)。 session.single_flight_user():同一用户的多步流程加 single-flight 闸,防并发竞态。- 跨会话配对用
Rendezvous<K, V>(带 TTL 的「一处发码、另一处认领」,如群里贴 token、私聊认领绑定)。
发消息
reply.text.await?; // 单段快捷:text / image / at / face
reply.reply.await?; // 同上,但引用触发消息
reply.msg.at.text.image_path // 混排链式:text / at / at_all / face
.send.await?; // / image_url / image_bytes / image_path
// / reply_to_trigger / push(Segment)
let m = new.text.image_url.build; // 脱离 Reply 拼 Message
bot.send.await?; // 交给 bot 发到任意会话
reply.send(&[Segment]) / reply.quote(..) 直接收段数组;链式收尾用 .send() 或 .quote()(带引用)。所有发送都返回 MessageId,可存下来供撤回/引用。
动作
bot.send.await?; // 通用动作:Bot 上的 inherent 方法
bot.group.member.mute.await?; // 类型化目标选择器
bot.actions.<vendor 专属方法>.await?; // 厂商/协议专属直达(OneBot 侧 145 个、Milky 侧 12 个)
当前端不支持的动作返回 Error::Unsupported(不会编译失败):bot.supports(Capability::..) 运行时探测、err.is_unsupported() 降级。连上后框架读 get_version_info 判定 Vendor(Lagrange / NapCat / LLOneBot / Other)自动选 per-vendor 动作名别名;Milky 端经 ImplInfo 识别。逃生口:入站解不出的内容降级 Event::Raw / Segment::Raw 不丢数据,出站有 call_raw 直发任意动作。
App
| 组 | 方法 |
|---|---|
| 注册 | command(matcher, h) / on(h) / on_top(h) 显式注册(与宏注册并存);command_with / on_with 带门控变体 |
| 状态 | data(T)(供 State<T> 取)、superusers([..]) |
| 行为 | debug()(parse-miss 回贴自动提示)、layer(中间件) |
| 开关持久化 | restore_switches(..) 启动恢复、on_switch_change(..) 变更回调 |
| 共享态句柄 | enabled_handle / cooldown_store_handle / rendezvous_handle / kill_switch_handle / sleep_handle …(在 run_* 前拿走,跨任务持有) |
| 运行 | run_onebot(cfg, shutdown) / run_milky(cfg, shutdown);ctrl_c_shutdown() 给一个 Ctrl-C 取消的 ShutdownToken |
协议与传输
协议端是真正登录 QQ、需你自行部署的外部程序;nagisa 连上它,并按其上报信息自动识别厂商、抹平各家动作名差异。已适配:
- OneBot v11:Lagrange.OneBot / NapCat / LLOneBot
- Milky:Lagrange.Milky / LLOneBot
连接这些协议端的传输方式:
| 协议 | 事件传输 | 构造 |
|---|---|---|
| OneBot v11 | 正向 WS(默认) | OneBotConfig::new(url) |
| OneBot v11 | 反向 WS(nagisa 作服务端) | OneBotConfig::reverse_ws(bind, path) |
| OneBot v11 | HTTP-POST webhook(X-Signature HMAC-SHA1) |
OneBotConfig::http(api_url, post_bind, post_path).with_secret(s) |
| OneBot v11 | 纯 HTTP-API(仅动作,无事件源) | OneBotConfig::http_api(api_url) |
| OneBot v11 | LLOneBot 私有 HTTP + SSE 推送 | OneBotConfig::llonebot_http_sse(api_url) |
| OneBot v11 | LLOneBot 私有 HTTP + 长轮询 | OneBotConfig::llonebot_http_long_poll(api_url, interval) |
| Milky | WS /event(默认) |
MilkyConfig::new(ws_url) |
| Milky | SSE /event |
MilkyConfig::new(ws_url).with_mode(MilkyMode::Sse) |
| Milky | WebHook(nagisa 作服务端,Bearer 鉴权) | MilkyConfig::new(ws_url).with_webhook(bind, path) |
OneBot 动作走同连接 echo 关联(WS),或把动作名接在 api_url 后 POST({api_url}/<action>);Milky 动作统一 POST 到 /api/<action>。访问令牌 .with_token(..)。
feature 与日志
| feature | 默认 | 作用 |
|---|---|---|
onebot |
是 | OneBot v11 适配器 |
milky |
是 | Milky 适配器 |
log |
否 | 日志门面 nagisa::log::* |
log feature 提供:init() 统一初始化(控制台 + 可选文件,返回的 LogGuard 须持有到退出;RUST_LOG 优先于配置)、EventLog 观察者(把事件渲染成可读日志行,含出站消息回显、名称缓存、撤回预览)、LogBus(log-as-event 广播)。
Workspace crate 地图
graph TD
APP["你的 bot 应用"] --> FAC["nagisa · 门面<br/>App + prelude"]
FAC --> CORE["nagisa-core · 引擎"]
FAC --> MAC["nagisa-macros · 宏"]
FAC -. feature onebot .-> OB["nagisa-onebot"]
FAC -. feature milky .-> MK["nagisa-milky"]
FAC -. feature log .-> LOG["nagisa-log"]
CORE --> TY["nagisa-types · 域模型"]
OB --> CORE
MK --> CORE
LOG --> CORE
OB --> TY
MK --> TY
LOG --> TY
分层视角(自顶向下,依赖向内收敛到 nagisa-types):
┌─ 你的 bot 应用 ── 数据 · 渲染 · 功能插件 ────────────────────┐
└──────────────────────────────┬───────────────────────────────┘
│ 依赖 nagisa(按需选 features)
┌──────────────────────────────▼──── nagisa(门面)─────────────────┐
│ App builder + prelude + re-export │
├──────────────────────────────────────────────────────────────────┤
│ nagisa-core 引擎:Ctx · 提取器 · Router · 门控 · Session/Waiter │
│ · Cooldown · Rendezvous · Service · Bot/Actions │
│ · 适配器共享基建(重连退避 / 读泵 / SSE / wire 日志) │
│ nagisa-macros #[command] / #[event] / plugin! / derive(Args/…) │
│ nagisa-types 统一域模型(纯数据,无 async) │
│ nagisa-log 事件日志 + 名称缓存(feature "log") │
├───────────────────────────┬──────────────────────────────────────┤
│ nagisa-onebot(OneBot v11) │ nagisa-milky(Milky) │
│ 6 种传输 │ WS / SSE / WebHook │
└───────────────────────────┴──────────────────────────────────────┘
↓ 外部进程 ↓ 外部进程
Lagrange.OneBot / NapCat Lagrange.Milky / …
/ LLOneBot
设计要点
- 防御式反序列化(在适配器 wire 层):
serde(default)+Option+#[serde(other)],从不deny_unknown_fields;解不出的入站事件降级Raw,不丢、不 panic。 - 错误口径:自有
Result/Error+Contexttrait(.context(..)/.with_context(..)归一 sea-orm / reqwest 等任意外部错误),不依赖anyhow;handler 内bail!("..")提前返回。handler 返回Err由框架捕获并记 warn 日志,分发继续、不自动回贴用户——要给用户反馈,返回前自行reply。动作失败按ActionErrorKind粗分类,不要比对具体 retcode 数字(各端不一致)。 - 加固:动作 echo 超时(10s)+ 挂起槽回收、断线指数退避重连(500ms → 30s)、连接 90s idle 看门狗、handler panic 任务级隔离、关停时全任务收束。
- 不透明
MessageId:OneBot 的 i32 与 Milky 的三元组都装得下,业务永不做算术/比较。
示例
crates/nagisa/examples/,cargo run --example <名字>(需要在线协议端):
| 示例 | 演示 |
|---|---|
echo_bot |
入门全览:命令 / 元素参数 / State<T> / #[derive(Args)] / #[derive(ArgEnum)] |
plugin_switches |
分层开关 + /enable /disable 管理命令 |
event_plugin |
事件触发(#[event(MemberJoin / Nudge)]),与命令同受开关门控 |
word_guess |
多轮会话(Session / Waiter + 置顶观察者计数) |
token_bind |
跨会话 Rendezvous(群里发码、私聊认领) |
完整真实工程见 abot-rs —— 一个基于 nagisa 搭建的多插件 QQ bot。
项目状态
早期项目,API 还会变。刻意零测试——门禁是 cargo build + cargo clippy -D warnings + cargo doc 零警告,正确性靠真实协议端连线实测。
最佳实践
description/usage当用户文案写——帮助菜单直接由registered_plugins()按category分组生成(跳过hidden),这两个字段就是用户看到的话,新插件零改动上架。- 参数宽进、handler 解释:
Args字段尽量Option,缺了什么用人话回复,而不是让框架硬拒。 - 插件自有数据不破墙:复用
nagisa::inventory搭自己的扩展点(插件自带数据库迁移、个人面板贡献槽,由核心统一收集),核心代码不引用任何具体插件。
可运行的最小用法见 crates/nagisa/examples/(命令/多轮会话/开关管理/事件处理各一例)。
License
MIT OR Apache-2.0