写在前面

2026年5月1日

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

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

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年4月13日

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

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

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


架构概述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────┐
│ 宿主 (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_send 插件 → 宿主 消息发送请求

指令解析(Command)

Command 库提供高效的指令前缀解析,支持自定义前缀字符和三种前缀模式。

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

基本用法

Rust

1
2
3
4
5
6
7
8
9
use luo9_sdk::command::{Command, PrefixMode};

match Command::parse(msg, "echo", PrefixMode::Required('/')) {
Some(cmd) => {
let args = cmd.args_raw();
Bot::send_private_msg(user_id, CString::new(args).unwrap());
}
None => return,
}

C++

1
2
3
4
5
6
7
8
#include "command.h"

auto cmd = Command::parse(msg, "echo", PrefixMode::Optional('/'));

if (!cmd.empty()) {
std::string args = cmd.args_raw();
Bot::send_private_msg(user_id, args);
}

链式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if let Some(cmd) = Command::parse(msg, "epic", PrefixMode::None) {
cmd.on("提醒开启", |_args| {
// 处理 "epic提醒开启"
})
.on("提醒关闭", |_args| {
// 处理 "epic提醒关闭"
})
.on("状态", |_args| {
// 处理 "epic状态"
})
.otherwise(|| {
// 无参数的 "epic"
});
}

常用方法

方法 Rust C++ 说明
获取指令名 cmd.name() cmd.name() 匹配到的指令名称
获取原始参数 cmd.args_raw() cmd.args_raw() 去除指令名后的参数字符串
是否有参数 cmd.has_args() cmd.has_args() 返回 bool
参数个数 cmd.args_count() cmd.args_count() 返回参数数量
获取指定参数 cmd.arg_at(0) cmd.arg_at(0) 返回第 n 个参数

消息发送(Bot)

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

1
2
3
4
5
6
// Rust
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());
1
2
3
4
5
// C++
#include "bot.h"

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

消息接收(Payload)

从总线接收的消息通过 BusPayload 解析:

1
2
3
4
5
6
7
8
9
10
11
12
use luo9_sdk::payload::BusPayload;

let json = Bus::topic("luo9_message").wait_pop(subscriber_id)?;
match BusPayload::parse(&json) {
Some(BusPayload::Message(msg)) => {
// msg.message_type: Private / Group
// msg.user_id, msg.group_id, msg.message
}
Some(BusPayload::MetaEvent(event)) => { /* 心跳/生命周期 */ }
Some(BusPayload::Notice(notice)) => { /* 通知事件 */ }
None => { /* 解析失败 */ }
}

定时任务(Task)

插件通过 luo9_task topic 注册/取消定时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注册定时任务
let request = serde_json::json!({
"action": "schedule",
"task_name": "my_task",
"cron": "0 0 9 * * *", // 每天 9:00
"payload": "任意数据"
});
Bus::topic("luo9_task").publish(&request.to_string())?;

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

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

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


插件开发模板

Rust

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
use luo9_sdk::bus::Bus;
use luo9_sdk::command::{Command, PrefixMode};
use luo9_sdk::payload::BusPayload;
use luo9_sdk::Bot;
use std::ffi::CString;

pub fn plugin_main() {
let sub_id = Bus::topic("luo9_message").subscribe().unwrap();

loop {
let json = Bus::topic("luo9_message").wait_pop(sub_id).unwrap();
if let Some(BusPayload::Message(msg)) = BusPayload::parse(&json) {
handle_message(msg.group_id.unwrap_or(0), msg.user_id, &msg.message);
}
}
}

fn handle_message(group_id: u64, user_id: u64, msg: &str) {
match Command::parse(msg, "echo", PrefixMode::Required('/')) {
Some(cmd) => {
let reply = CString::new(cmd.args_raw()).unwrap();
Bot::send_group_msg(group_id, reply);
}
None => return,
};
}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "bus.h"
#include "command.h"
#include "bot.h"
#include "payload.h"

void plugin_main() {
auto sub_id = Bus::topic("luo9_message").subscribe();

while (true) {
auto json = Bus::topic("luo9_message").wait_pop(sub_id);
auto payload = BusPayload::parse(json);

if (payload.is_message()) {
handle_message(payload.group_id(), payload.user_id(), payload.message());
}
}
}

void handle_message(uint64_t group_id, uint64_t user_id, const std::string& msg) {
auto cmd = Command::parse(msg, "echo", PrefixMode::Required('/'));
if (!cmd.empty()) {
Bot::send_group_msg(group_id, cmd.args_raw());
}
}

FFI 接口

插件必须导出:

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

luo9_core 暴露的核心 FFI 函数:

函数 用途
luo9_bus_init() 初始化总线单例
luo9_bus_subscribe(topic) 订阅 topic,返回 subscriber_id
luo9_bus_publish(topic, payload) 发布消息
luo9_bus_pop(topic, sub_id) 非阻塞取消息
luo9_bus_wait_pop(topic, sub_id) 阻塞取消息
luo9_bus_free_string(ptr) 释放 bus 返回的字符串

群聊事件

群消息通过 luo9_message topic 推送,message_type"group"

私聊事件

私聊消息通过 luo9_message topic 推送,message_type"private"

通知事件

事件名称 事件含义
friend_add 好友添加
friend_recall 好友撤回
group_admin 群管理员变动
group_ban 群禁言
group_increase 群成员增加
group_decrease 群成员减少
group_card 群名片变更
group_recall 群消息撤回
group_upload 群文件上传
poke 戳一戳