Skip to content

适配器开发

适配器是一种特殊的插件,负责对接聊天平台(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);
}

部署

  1. cargo build --release
  2. .dll / .so 复制到 plugins/ 目录
  3. 重启 FoxCore(首次加载时自动生成带注释的默认配置)
  4. 编辑 config/plugins/<名称>.toml,再次重启