惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
S
Securelist
博客园 - Franky
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
IT之家
IT之家
GbyAI
GbyAI
Microsoft Azure Blog
Microsoft Azure Blog
The Cloudflare Blog
云风的 BLOG
云风的 BLOG
N
News and Events Feed by Topic
AI
AI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Schneier on Security
Schneier on Security
Attack and Defense Labs
Attack and Defense Labs
Vercel News
Vercel News
腾讯CDC
Google DeepMind News
Google DeepMind News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
M
MIT News - Artificial intelligence
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
量子位
S
Schneier on Security
Hacker News: Ask HN
Hacker News: Ask HN
Cyberwarzone
Cyberwarzone
S
Security Affairs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News and Events Feed by Topic
T
Tenable Blog
PCI Perspectives
PCI Perspectives
MyScale Blog
MyScale Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
W
WeLiveSecurity
N
News | PayPal Newsroom
P
Proofpoint News Feed
O
OpenAI News
C
CERT Recently Published Vulnerability Notes
B
Blog
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
MongoDB | Blog
MongoDB | Blog
大猫的无限游戏
大猫的无限游戏
A
Arctic Wolf
Y
Y Combinator Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Spread Privacy
Spread Privacy

洛屿的小站

STM32F407 IAR环境下uC/OS-III移植完整指南 STM32 UCOS3+HAL库集成中的FPU HardFault问题 一种简单的卡尔曼滤波器设计 Cadence - 应律而动,落指成音 Zephyr - 聆风之息,为你而奏 不改一行插件代码,实现消息优先级与阻断 洛玖定时任务系统 在 butterfly 主题中添加首页点集动画(基于p2line项目) 你好,2026 stm32f4xx-ads1256驱动 stm32f4xx-ad9854并行驱动 主动式网站状态监测实现及其应用 右键菜单加入用Trae打开文件和文件夹 三角洲行动ID映射表 为网页文章开头添加原文连接 Hexo-Butterfly主题在主页添加GitHub贡献日历 Proteus中555定时器仿真问题 装饰器 洛玖开发日记 STS3032舵机获取力矩输出 kotlin网页前后端那些事 mspm0g3507-ad9850 奇怪的bug Paddle模型转PaddleLite 人工智能考核 构建一个yolov3网络 yolo和paddle模型常见输出参数 PaddleDetection 随便写的一些东西 实验室C语言第一次考核题目讲解及相关代码解读 C语言神经网络房价预测系统 C、C++数组,指针,指针数组,数组指针的区别 C、C++的大括号是必须的部分吗? C、C++预处理详解 C、C++其他关键字详解 C、C++存储类型关键字详解 C、C++控制语句关键字详解 C、C++数据类型关键字详解 C、C++关键字 C++药品管理系统 Bi-LSTM(Attention)的PyTorch实现 C语言实现波士顿房价预测 easy库的使用 edgeboardFZ3A相关问题 Predict.py的编写 Paddle环境搭建 百度Paddle模型训练 GMD09601-0.96OLED显示屏 【LeetCode 1617】统计子树中城市之间最大距离 C语言学习相关 STM32阵列按键 CH32(F10X F20X) 最短路 拓扑排序
洛玖SDK说明
洛屿 · 2025-03-31 · via 洛屿的小站

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

写在前面

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

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

随着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 的入口点