适配器开发
适配器是一种特殊的插件,负责对接聊天平台(QQ、Discord 等),将平台消息转换为 FoxCore 通用格式。
实现 Adapter trait
rust
use foxcore_api::*;
use tokio::sync::RwLock;
use std::sync::Arc;
pub struct MyAdapter {
config: MyConfig,
}
#[async_trait::async_trait]
impl Adapter for MyAdapter {
fn name(&self) -> &str {
"my_platform"
}
async fn start(&self, callback: Box<dyn AdapterCallback>) -> Result<(), AdapterError> {
// 建立连接,spawn 后台任务接收消息
// 收到消息时调用 callback.emit(AdapterEvent::MessageReceived(...))
Ok(())
}
async fn send_message(&self, message: OutgoingMessage) -> Result<String, AdapterError> {
// 将通用 OutgoingMessage 转换为平台格式并发送
// 返回平台分配的消息 ID
Ok("msg_id".to_string())
}
async fn stop(&self) -> Result<(), AdapterError> {
// 关闭连接,清理资源
Ok(())
}
}导出入口函数
每个适配器 dylib 必须导出此函数,核心通过它创建适配器实例:
rust
#[unsafe(no_mangle)]
pub extern "Rust" fn foxcore_create_adapter(config_toml: &str) -> Box<dyn Adapter> {
let config: MyConfig = toml::from_str(config_toml).unwrap_or_default();
Box::new(MyAdapter::new(config))
}核心启动时会读取 config/plugins/<适配器名>.toml,将 TOML 原文传入此函数。
定义配置
rust
use foxcore_api::Config;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct MyConfig {
pub version: u32,
pub server_url: String,
pub token: String,
}
impl Default for MyConfig { /* ... */ }
impl Config for MyConfig {
fn version() -> u32 { 1 }
fn file_name() -> &'static str { "my_adapter.toml" }
}上报事件
通过 AdapterCallback 向核心上报事件:
rust
// 收到消息
callback.emit(AdapterEvent::MessageReceived(IncomingMessage {
message_id: "123".to_string(),
adapter: "my_platform".to_string(),
context: MessageContext::Group { group_id: "456".to_string() },
sender: Sender { user_id: "789".to_string(), nickname: "Alice".to_string() },
segments: vec![MessageSegment::Text { text: "hello".to_string() }],
plain_text: "hello".to_string(),
timestamp: 1700000000,
})).await;
// 连接成功
callback.emit(AdapterEvent::Connected).await;
// 断开连接
callback.emit(AdapterEvent::Disconnected { reason: "timeout".to_string() }).await;通用消息段
适配器需要将平台特有格式转换为以下通用段:
| 段类型 | 说明 |
|---|---|
Text { text } | 纯文本 |
Image { url } | 图片 |
Mention { target_id } | @某人 |
Reply { message_id } | 回复消息 |
Custom { kind, data } | 平台特有内容透传 |
平台特有的消息类型(如 QQ 表情、语音、视频等)使用 Custom 段透传,kind 为类型名,data 为 JSON 数据。
Tokio Runtime
重要
适配器编译为 dylib,拥有独立的 tokio 拷贝,不共享核心的 tokio runtime。 直接调用 tokio::spawn() 会 panic。
适配器必须在 start() 中自建 runtime:
rust
async fn start(&self, callback: Box<dyn AdapterCallback>) -> Result<(), AdapterError> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.thread_name("my-adapter")
.build()
.map_err(|e| AdapterError::new(format!("创建运行时失败:{e}")))?;
// 在自己的 runtime 上 spawn 连接任务
rt.spawn(async move {
// 所有异步操作在这里执行
});
// 保存 runtime 防止 drop(用 std::sync::RwLock,不是 tokio 的)
*self.runtime.write().unwrap() = Some(rt);
Ok(())
}Cargo.toml 中 tokio 必须开启 rt-multi-thread:
toml
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }call_api
适配器可选实现 call_api 方法,让核心组件调用平台特有的 API:
rust
async fn call_api(&self, action: &str, params: serde_json::Value) -> Result<serde_json::Value, AdapterError> {
// 转发给底层连接
self.connection.call_api(action, params).await
}核心组件通过 BusContext 间接调用:
rust
// 撤回消息
ctx.call_adapter_api("delete_msg", json!({"message_id": 12345})).await?;
// 群禁言
ctx.call_adapter_api("set_group_ban", json!({
"group_id": "123456", "user_id": "789", "duration": 600
})).await?;日志
适配器 dylib 不能用 tracing::info!(跨 dylib 不共享 subscriber)。使用 foxcore-api 提供的日志宏:
rust
use foxcore_api::{log_info, log_warn, log_error};
log_info!("连接成功");
log_info!("cyan"; "带颜色的日志");适配器需导出日志初始化函数:
rust
#[unsafe(no_mangle)]
pub extern "Rust" fn foxcore_set_logger(log_fn: foxcore_api::adapter::CoreLogFn) {
foxcore_api::log::set_log_fn(log_fn);
}部署
cargo build --release- 将
.dll/.so复制到plugins/目录 - 重启 FoxCore(首次加载时自动生成带注释的默认配置)
- 编辑
config/plugins/<名称>.toml,再次重启