注意:目前 SDK 的开发重心集中在 Rust 部分,C++ 和 Python SDK 暂未同步更新。
本项目作为一个非盈利项目,如果你有兴趣参与 C++ 或 Python SDK 的维护与开发,亦或者参与luo9_bot本身的开发,欢迎通过邮件联系:luoy-oss@qq.com

写在前面

2026年5月6日

Rust SDK v0.6.0:新增优先级定向分发、热重载(sentinel 机制)、Pattern 模式匹配、Version 查询协议

luo9_bot:luoy-oss/luo9_bot

Rust SDK:luo9-bot/luo9_sdk_rust

C++ SDK:luo9-bot/luo9_sdk_cpp

Rust 插件样例:luo9-bot/plugin-rust-example

C++ 插件样例:luo9-bot/plugin-cpp-example

2026年5月1日

已完成 FFI 消息总线架构,插件通过 luo9_coreextern "C" 函数进行进程内 pub/sub 通信

已完成多语言 SDK(Rust / C++ / Python),均封装同一份 luo9_core FFI

2026年4月13日

随着rust的学习推进,发现旧版本的rust实现较为劣质,现阶段正在进行重构

本项目不作为核心项目开发,项目的迭代时间并不确定

请不要使用以下sdk,当前页面仅作保留,后续将继续使用该页面进行sdk更新跟进


架构概述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────┐
│ 宿主 (luo9_bot, Rust) │
│ ├── WebSocket 连接 (Napcat) │
│ ├── 事件路由 (message/event/notice) │
│ ├── 插件加载器 (DLL/SO) │
│ ├── 优先级分发 + 热重载 │
│ └── Cron 调度器 │
└─────────────────┬───────────────────────────┘
│ FFI 消息总线 (luo9_core)

┌─────────────────┴───────────────────────────┐
│ 插件 (DLL/SO, 任意语言) │
│ ├── 独立 OS 线程运行 │
│ ├── 通过 Bus 订阅/发布消息 │
│ └── 通过 Bot 发送消息 │
└─────────────────────────────────────────────┘

总线 Topic

Topic 方向 用途
luo9_message 宿主 → 插件 QQ 消息(私聊/群聊)
luo9_meta_event 宿主 → 插件 元事件(心跳/生命周期)
luo9_notice 宿主 → 插件 通知事件(好友/群变动等)
luo9_task 宿主 → 插件 定时任务触发事件
luo9_task_miso 插件 → 宿主 定时任务注册/取消请求
luo9_send 插件 → 宿主 消息发送请求
luo9_version 双向 版本查询/响应

热重载支持

宿主在加载插件时预创建 subscriber,通过 luo9_init_subscribers FFI 将 ID 传递给 SDK。插件调用 subscribe() 时,SDK 内部透明返回预分配的 ID——插件代码无需任何修改

禁用插件时,宿主调用 unsubscribe() 将 subscriber 标记为 dead,SDK 的 wait_pop() 会识别哨兵消息 __luo9_unsubscribed__ 并返回 Err(BusError::Unsubscribed),插件循环自然退出,线程结束,动态库被卸载。


消息总线(Bus)

Bus 是 SDK 的核心,封装了 luo9_core 的 FFI 消息总线。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use luo9_sdk::bus::Bus;

// 订阅 topic
let sub_id = Bus::topic("luo9_message").subscribe()?;

// 非阻塞取消息(配合 thread::sleep 使用)
if let Some(json) = Bus::topic("luo9_message").pop(sub_id) {
// 处理消息
}

// 阻塞取消息(线程挂起直到有消息)
let json = Bus::topic("luo9_message").wait_pop(sub_id)?;

// 发布消息(广播给所有 subscriber)
Bus::topic("luo9_send").publish(r#"{"action":"send_group_msg","group_id":123,"message":"hello"}"#)?;

// 定向推送给指定 subscriber
Bus::topic("luo9_message").publish_to(&payload, &[0, 2])?;

// 取消订阅
Bus::topic("luo9_message").unsubscribe(sub_id)?;

BusError

1
2
3
4
5
6
7
8
9
10
pub enum BusError {
InitFailed, // 总线初始化失败
PublishFailed, // 发布失败
SubscribeFailed, // 订阅失败
WaitPopFailed, // 阻塞取消息失败
NotInitialized, // 总线未初始化
InvalidString, // 字符串转换失败
UnsubscribeFailed, // 取消订阅失败
Unsubscribed, // subscriber 已被取消订阅(热重载哨兵)
}

pop() 在 subscriber 被取消订阅后返回 None(哨兵被视为无消息);wait_pop() 在 subscriber 被取消订阅后返回 Err(BusError::Unsubscribed),插件可据此退出循环。

API 速查

方法 返回值 说明
Bus::init() Result<(), BusError> 初始化总线
Bus::topic(name) Topic 获取 topic 句柄
topic.subscribe() Result<usize, BusError> 订阅,返回 subscriber_id
topic.unsubscribe(id) Result<(), BusError> 取消订阅
topic.publish(payload) Result<(), BusError> 广播消息
topic.publish_to(payload, ids) Result<(), BusError> 定向推送
topic.pop(id) Option<String> 非阻塞取消息
topic.wait_pop(id) Result<String, BusError> 阻塞取消息
topic.publish_fmt(payload) Result<(), BusError> 发布(自动 to_string)

指令解析(Command)

Command 提供高效的指令前缀解析,支持链式匹配和模式匹配。

前缀模式

模式 说明 示例
Required(char) 必须有指定前缀才解析成功 /echo hello
Optional(char) 前缀可选 echo hello/echo hello
None 不检查前缀,直接解析 echo hello

基本用法

1
2
3
4
5
use luo9_sdk::command::{Command, PrefixMode};

if let Some(cmd) = Command::parse(msg, "echo", PrefixMode::Required('/')) {
Bot::send_group_msg(group_id, CString::new(cmd.args().join(" ")).unwrap());
}

链式匹配

cmd.on() 匹配第一个参数(子指令),匹配成功时执行闭包并传入剩余参数。可链式调用多个 .on()

1
2
3
4
5
6
7
8
if let Some(cmd) = Command::parse(msg, "task", PrefixMode::Required('/')) {
cmd.on("start", |args| {
// 处理 "/task start ..."
})
.on("end", |args| {
// 处理 "/task end ..."
});
}

默认处理器(handle)

cmd.handle() 在命令无参数时执行闭包,返回 CommandMatcher 可继续链式调用 .on()。适合”有参数走子命令,无参数走默认”的场景:

1
2
3
4
5
6
7
8
9
10
11
12
if let Some(cmd) = Command::parse(msg, "epic", PrefixMode::None) {
cmd.handle(|| {
// 无参数的 "epic" → 查询免费游戏
epic_free_games(group_id);
})
.on("提醒开启", |_| {
task_start(group_id);
})
.on("提醒关闭", |_| {
task_end(group_id);
});
}

otherwise 兜底

.otherwise() 在所有 .on() 都未匹配时执行,适合兜底逻辑:

1
2
3
4
5
6
7
8
9
if let Some(cmd) = Command::parse(msg, "doro", PrefixMode::None) {
cmd.on("add", |args| { /* doro add ... */ })
.on("remove", |args| { /* doro remove ... */ })
.on("list", |_| { /* doro list */ })
.otherwise(|| {
// 无匹配子命令时的默认行为
send_today(group_id, &uid);
});
}

模式匹配(on_pattern)

on_pattern 支持带命名捕获的模板,用 {name} 标记捕获组。适合解析 CQ Code 等结构化消息:

1
2
3
4
5
cmd.on_pattern("[CQ:at,qq={qq}]{content}", |caps, _args| {
let qq = caps.get("qq").unwrap();
let content = caps.get("content").unwrap();
// 处理 @消息
});

API 速查

方法 返回值 说明
Command::parse(msg, name, mode) Option<Command> 解析指令
cmd.name() &str 指令名称
cmd.args() &[String] 参数列表
cmd.arg_at(n) Option<&str> 第 n 个参数
cmd.args_from(start) &[String] 从第 start 个开始的参数
cmd.args_raw() String 原始参数字符串
cmd.has_args() bool 是否有参数
cmd.args_count() usize 参数个数
cmd.handle(f) CommandMatcher 无参数时执行 f
cmd.on(expected, f) CommandMatcher 子指令匹配
cmd.on_pattern(pattern, f) CommandMatcher 模式匹配
matcher.on(expected, f) CommandMatcher 链式子指令
matcher.on_pattern(pattern, f) CommandMatcher 链式模式匹配
matcher.otherwise(f) () 兜底处理

消息构建(MsgBuilder)

MsgBuilder 提供流式 API 构建 CQ Code 消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use luo9_sdk::Msg;

// 纯文本
let msg = Msg::txt("你好").build();
Bot::send_group_msg(group_id, msg);

// 图片(自动检测 URL / 本地文件)
let msg = Msg::image("https://example.com/img.png").build();

// 复合消息:文本 + 换行 + 文本 + 图片
let msg = Msg::txt("游戏名称: Epic Game").endl()
.txt("描述: 一款好游戏").endl()
.txt("预览图片:").image(game.preview_image)
.build();
Bot::send_group_msg(group_id, msg);
方法 说明
Msg::new(text) 创建 builder,初始文本
Msg::txt(text) 创建 builder,初始纯文本
Msg::image(path) 创建 builder,初始图片
Msg::at(user_id) 创建 builder,初始 @某人
.txt(text) 追加文本
.at(user_id) 追加 @
.image(path) 追加图片(含 httpurl,否则用 file
.endl() 追加换行
.build() 构建为 CString

消息发送(Bot)

插件通过 Bot 发送消息,内部通过 luo9_send topic 传递给宿主:

1
2
3
4
5
use luo9_sdk::Bot;
use std::ffi::CString;

Bot::send_group_msg(group_id, CString::new("你好").unwrap());
Bot::send_private_msg(user_id, CString::new("你好").unwrap());

也可以使用 send 模块的函数(接受 &str,更简洁):

1
2
3
4
use luo9_sdk::send;

send::send_group_msg(group_id, "你好");
send::send_private_msg(user_id, "你好");

消息接收(Payload)

从总线接收的 JSON 通过 BusPayload::parse() 解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use luo9_sdk::payload::*;

if let Some(BusPayload::Message(msg)) = BusPayload::parse(&json) {
match msg.message_type {
MsgType::Group => {
let group_id = msg.group_id.unwrap_or(0);
let user_id = msg.user_id;
let content = &msg.message;
}
MsgType::Private => {
let user_id = msg.user_id;
let content = &msg.message;
}
_ => {}
}
}

类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pub enum MsgType { Private, Group, Other }

pub struct MessagePayload {
pub message_type: MsgType,
pub user_id: u64,
pub group_id: Option<u64>,
pub message: String,
}

pub enum MetaEventType { Lifecycle, Heartbeat, Unknown }

pub struct MetaEventPayload {
pub interval: Option<u64>,
pub meta_event_type: MetaEventType,
pub sub_type: SubType,
pub self_id: u64,
pub status: Option<Status>,
pub time: u64,
}

pub enum NoticeType {
FriendAdd, FriendRecall, GroupAdmin, GroupBan,
GroupIncrease, GroupDecrease, GroupCard, GroupRecall,
GroupUpload, Essence, Notify, Unknown,
}

pub struct NoticePayload {
pub notice_type: NoticeType,
pub sub_type: SubType,
pub status: String,
pub user_id: u64,
pub group_id: Option<u64>,
pub time: u64,
}

模式匹配(Pattern)

Pattern 提供带命名捕获的字符串模板匹配,{name} 为捕获组:

1
2
3
4
5
6
7
use luo9_sdk::pattern::Pattern;

let pat = Pattern::new("[CQ:at,qq={qq}]{content}");
if let Some(caps) = pat.match_str("[CQ:at,qq=123456]你好呀") {
let qq = caps.get("qq").unwrap(); // "123456"
let content = caps.get("content").unwrap(); // "你好呀"
}

定时任务(Task)

插件通过 luo9_task_miso topic 注册/取消定时任务,通过 luo9_task topic 接收触发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use luo9_sdk::bus::Bus;
use serde_json::json;

// 注册定时任务(发布到 luo9_task_miso)
let req = json!({
"action": "schedule",
"task_name": "my_task",
"cron": "0 0 8 * * *", // 每天 8:00
"payload": group_id.to_string()
});
Bus::topic("luo9_task_miso").publish(&req.to_string())?;

// 取消定时任务
let cancel = json!({
"action": "cancel",
"task_name": "my_task"
});
Bus::topic("luo9_task_miso").publish(&cancel.to_string())?;

// 接收定时事件(订阅 luo9_task 后)
// 收到的 JSON: {"event": "tick", "task_name": "my_task", "payload": "..."}

Cron 表达式为 6 字段格式 秒 分 时 日 月 周,支持 ? L W # 等特殊字符。详见 洛玖定时任务系统


版本查询(Version)

宿主可以查询插件的版本信息。插件订阅 luo9_version topic,收到查询后回复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use luo9_sdk::version;
use luo9_sdk::bus::Bus;

let ver_sub = Bus::topic("luo9_version").subscribe()?;
let ver_topic = Bus::topic("luo9_version");

loop {
if let Some(json) = ver_topic.pop(ver_sub) {
if version::is_version_query(&json) {
version::reply_version(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
函数 说明
version::is_version_query(json) 判断是否为版本查询请求
version::reply_version(name, version) 发布版本查询响应

插件开发模板

Cargo.toml

1
2
3
4
5
6
7
8
9
10
11
[package]
name = "my_plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
luo9_sdk = "0.6.0"
serde_json = "1"

src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
mod core;

use luo9_sdk::Bot;
use luo9_sdk::bus::Bus;
use luo9_sdk::command::{Command, PrefixMode};
use luo9_sdk::payload::*;
use std::ffi::CString;

#[unsafe(no_mangle)]
pub extern "C" fn plugin_main() {
// 订阅需要的 topic
let msg_sub = Bus::topic("luo9_message").subscribe().unwrap();
let ver_sub = Bus::topic("luo9_version").subscribe().unwrap();

// 缓存 Topic 句柄(避免重复创建 CString)
let msg_topic = Bus::topic("luo9_message");
let ver_topic = Bus::topic("luo9_version");

println!("[my_plugin] 插件已启动");

loop {
// 消息处理
if let Some(json) = msg_topic.pop(msg_sub) {
if let Some(BusPayload::Message(msg)) = BusPayload::parse(&json) {
match msg.message_type {
MsgType::Group => {
handle_group_msg(msg.group_id.unwrap_or(0), msg.user_id, &msg.message);
}
MsgType::Private => {
handle_private_msg(msg.user_id, &msg.message);
}
_ => {}
}
}
}

// 版本查询
if let Some(json) = ver_topic.pop(ver_sub) {
if luo9_sdk::version::is_version_query(&json) {
luo9_sdk::version::reply_version(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
);
}
}

// 短暂让出 CPU,避免空转
std::thread::sleep(std::time::Duration::from_millis(1));
}
}

fn handle_group_msg(group_id: u64, _user_id: u64, msg: &str) {
if let Some(cmd) = Command::parse(msg, "echo", PrefixMode::Required('/')) {
Bot::send_group_msg(group_id, CString::new(cmd.args().join(" ")).unwrap());
}
}

fn handle_private_msg(user_id: u64, msg: &str) {
if let Some(cmd) = Command::parse(msg, "echo", PrefixMode::Required('/')) {
Bot::send_private_msg(user_id, CString::new(cmd.args().join(" ")).unwrap());
}
}

构建

1
2
3
cargo build --release
# 产物:target/release/my_plugin.dll (Windows) / libmy_plugin.so (Linux)
# 将产物放入宿主的 plugins/ 目录即可

FFI 接口

插件必须导出:

1
extern "C" fn plugin_main();  // 在独立线程中运行

宿主通过 luo9_init_subscribers 传递预分配的 subscriber ID:

1
2
3
4
5
6
7
8
#[repr(C)]
pub struct PluginSubscribers {
pub message_sub_id: i32,
pub meta_event_sub_id: i32,
pub notice_sub_id: i32,
pub task_sub_id: i32,
pub send_sub_id: i32,
}

luo9_core 暴露的核心 FFI 函数:

函数 用途
luo9_bus_init() 初始化总线单例
luo9_bus_subscribe(topic) 订阅 topic,返回 subscriber_id
luo9_bus_unsubscribe(topic, sub_id) 取消订阅,唤醒阻塞线程(sentinel 机制)
luo9_bus_publish(topic, payload) 广播消息给所有 subscriber
luo9_bus_publish_to(topic, payload, ids, len) 定向推送消息给指定 subscriber
luo9_bus_pop(topic, sub_id) 非阻塞取消息
luo9_bus_wait_pop(topic, sub_id) 阻塞取消息(dead subscriber 返回 sentinel)
luo9_bus_free_string(ptr) 释放 bus 返回的字符串

模块总览

模块 说明
bus 消息总线封装:订阅、发布、定向推送、取消订阅
command 指令解析:前缀匹配、链式匹配、模式匹配
message MsgBuilder:流式构建 CQ Code 消息
payload 消息反序列化:Message / MetaEvent / Notice
send 消息发送:send_group_msg / send_private_msg
pattern 模式匹配:带命名捕获的字符串模板
version 版本查询协议
Bot 消息发送 + 版本查询的高层封装
Msg MsgBuilder 的入口点