feat(phase4): complete zclaw-skills, zclaw-hands, zclaw-channels, zclaw-protocols 模块实现
This commit is contained in:
71
crates/zclaw-channels/src/adapters/console.rs
Normal file
71
crates/zclaw-channels/src/adapters/console.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! Console channel adapter for testing
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use crate::{Channel, ChannelConfig, ChannelStatus, IncomingMessage, OutgoingMessage};
|
||||
|
||||
/// Console channel adapter (for testing)
|
||||
pub struct ConsoleChannel {
|
||||
config: ChannelConfig,
|
||||
status: Arc<tokio::sync::RwLock<ChannelStatus>>,
|
||||
}
|
||||
|
||||
impl ConsoleChannel {
|
||||
pub fn new(config: ChannelConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
status: Arc::new(tokio::sync::RwLock::new(ChannelStatus::Disconnected)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for ConsoleChannel {
|
||||
fn config(&self) -> &ChannelConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
async fn connect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Connected;
|
||||
tracing::info!("Console channel connected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Disconnected;
|
||||
tracing::info!("Console channel disconnected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status(&self) -> ChannelStatus {
|
||||
self.status.read().await.clone()
|
||||
}
|
||||
|
||||
async fn send(&self, message: OutgoingMessage) -> Result<String> {
|
||||
// Print to console for testing
|
||||
let msg_id = format!("console_{}", chrono::Utc::now().timestamp());
|
||||
|
||||
match &message.content {
|
||||
crate::MessageContent::Text { text } => {
|
||||
tracing::info!("[Console] To {}: {}", message.conversation_id, text);
|
||||
}
|
||||
_ => {
|
||||
tracing::info!("[Console] To {}: {:?}", message.conversation_id, message.content);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
async fn receive(&self) -> Result<mpsc::Receiver<IncomingMessage>> {
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
// Console channel doesn't receive messages automatically
|
||||
// Messages would need to be injected via a separate method
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
57
crates/zclaw-channels/src/adapters/discord.rs
Normal file
57
crates/zclaw-channels/src/adapters/discord.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Discord channel adapter
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use crate::{Channel, ChannelConfig, ChannelStatus, IncomingMessage, OutgoingMessage};
|
||||
|
||||
/// Discord channel adapter
|
||||
pub struct DiscordChannel {
|
||||
config: ChannelConfig,
|
||||
status: Arc<tokio::sync::RwLock<ChannelStatus>>,
|
||||
}
|
||||
|
||||
impl DiscordChannel {
|
||||
pub fn new(config: ChannelConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
status: Arc::new(tokio::sync::RwLock::new(ChannelStatus::Disconnected)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for DiscordChannel {
|
||||
fn config(&self) -> &ChannelConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
async fn connect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Connected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Disconnected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status(&self) -> ChannelStatus {
|
||||
self.status.read().await.clone()
|
||||
}
|
||||
|
||||
async fn send(&self, _message: OutgoingMessage) -> Result<String> {
|
||||
// TODO: Implement Discord API send
|
||||
Ok("discord_msg_id".to_string())
|
||||
}
|
||||
|
||||
async fn receive(&self) -> Result<mpsc::Receiver<IncomingMessage>> {
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
// TODO: Implement Discord gateway
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
11
crates/zclaw-channels/src/adapters/mod.rs
Normal file
11
crates/zclaw-channels/src/adapters/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! Channel adapters
|
||||
|
||||
mod telegram;
|
||||
mod discord;
|
||||
mod slack;
|
||||
mod console;
|
||||
|
||||
pub use telegram::TelegramChannel;
|
||||
pub use discord::DiscordChannel;
|
||||
pub use slack::SlackChannel;
|
||||
pub use console::ConsoleChannel;
|
||||
57
crates/zclaw-channels/src/adapters/slack.rs
Normal file
57
crates/zclaw-channels/src/adapters/slack.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Slack channel adapter
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use crate::{Channel, ChannelConfig, ChannelStatus, IncomingMessage, OutgoingMessage};
|
||||
|
||||
/// Slack channel adapter
|
||||
pub struct SlackChannel {
|
||||
config: ChannelConfig,
|
||||
status: Arc<tokio::sync::RwLock<ChannelStatus>>,
|
||||
}
|
||||
|
||||
impl SlackChannel {
|
||||
pub fn new(config: ChannelConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
status: Arc::new(tokio::sync::RwLock::new(ChannelStatus::Disconnected)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for SlackChannel {
|
||||
fn config(&self) -> &ChannelConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
async fn connect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Connected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Disconnected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status(&self) -> ChannelStatus {
|
||||
self.status.read().await.clone()
|
||||
}
|
||||
|
||||
async fn send(&self, _message: OutgoingMessage) -> Result<String> {
|
||||
// TODO: Implement Slack API send
|
||||
Ok("slack_msg_ts".to_string())
|
||||
}
|
||||
|
||||
async fn receive(&self) -> Result<mpsc::Receiver<IncomingMessage>> {
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
// TODO: Implement Slack RTM/events API
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
59
crates/zclaw-channels/src/adapters/telegram.rs
Normal file
59
crates/zclaw-channels/src/adapters/telegram.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Telegram channel adapter
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use crate::{Channel, ChannelConfig, ChannelStatus, IncomingMessage, OutgoingMessage};
|
||||
|
||||
/// Telegram channel adapter
|
||||
pub struct TelegramChannel {
|
||||
config: ChannelConfig,
|
||||
client: Option<reqwest::Client>,
|
||||
status: Arc<tokio::sync::RwLock<ChannelStatus>>,
|
||||
}
|
||||
|
||||
impl TelegramChannel {
|
||||
pub fn new(config: ChannelConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
client: None,
|
||||
status: Arc::new(tokio::sync::RwLock::new(ChannelStatus::Disconnected)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for TelegramChannel {
|
||||
fn config(&self) -> &ChannelConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
async fn connect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Connected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect(&self) -> Result<()> {
|
||||
let mut status = self.status.write().await;
|
||||
*status = ChannelStatus::Disconnected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn status(&self) -> ChannelStatus {
|
||||
self.status.read().await.clone()
|
||||
}
|
||||
|
||||
async fn send(&self, _message: OutgoingMessage) -> Result<String> {
|
||||
// TODO: Implement Telegram API send
|
||||
Ok("telegram_msg_id".to_string())
|
||||
}
|
||||
|
||||
async fn receive(&self) -> Result<mpsc::Receiver<IncomingMessage>> {
|
||||
let (tx, rx) = mpsc::channel(100);
|
||||
// TODO: Implement Telegram webhook/polling
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
94
crates/zclaw-channels/src/bridge.rs
Normal file
94
crates/zclaw-channels/src/bridge.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! Channel bridge manager
|
||||
//!
|
||||
//! Coordinates multiple channel adapters and routes messages.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use super::{Channel, ChannelConfig, ChannelStatus, IncomingMessage, OutgoingMessage};
|
||||
|
||||
/// Channel bridge manager
|
||||
pub struct ChannelBridge {
|
||||
channels: RwLock<HashMap<String, Arc<dyn Channel>>>,
|
||||
configs: RwLock<HashMap<String, ChannelConfig>>,
|
||||
}
|
||||
|
||||
impl ChannelBridge {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
channels: RwLock::new(HashMap::new()),
|
||||
configs: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a channel adapter
|
||||
pub async fn register(&self, channel: Arc<dyn Channel>) {
|
||||
let config = channel.config().clone();
|
||||
let mut channels = self.channels.write().await;
|
||||
let mut configs = self.configs.write().await;
|
||||
|
||||
channels.insert(config.id.clone(), channel);
|
||||
configs.insert(config.id.clone(), config);
|
||||
}
|
||||
|
||||
/// Get a channel by ID
|
||||
pub async fn get(&self, id: &str) -> Option<Arc<dyn Channel>> {
|
||||
let channels = self.channels.read().await;
|
||||
channels.get(id).cloned()
|
||||
}
|
||||
|
||||
/// Get channel configuration
|
||||
pub async fn get_config(&self, id: &str) -> Option<ChannelConfig> {
|
||||
let configs = self.configs.read().await;
|
||||
configs.get(id).cloned()
|
||||
}
|
||||
|
||||
/// List all channels
|
||||
pub async fn list(&self) -> Vec<ChannelConfig> {
|
||||
let configs = self.configs.read().await;
|
||||
configs.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Connect all channels
|
||||
pub async fn connect_all(&self) -> Result<()> {
|
||||
let channels = self.channels.read().await;
|
||||
for channel in channels.values() {
|
||||
channel.connect().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disconnect all channels
|
||||
pub async fn disconnect_all(&self) -> Result<()> {
|
||||
let channels = self.channels.read().await;
|
||||
for channel in channels.values() {
|
||||
channel.disconnect().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send message through a specific channel
|
||||
pub async fn send(&self, channel_id: &str, message: OutgoingMessage) -> Result<String> {
|
||||
let channel = self.get(channel_id).await
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Channel not found: {}", channel_id)))?;
|
||||
|
||||
channel.send(message).await
|
||||
}
|
||||
|
||||
/// Remove a channel
|
||||
pub async fn remove(&self, id: &str) {
|
||||
let mut channels = self.channels.write().await;
|
||||
let mut configs = self.configs.write().await;
|
||||
|
||||
channels.remove(id);
|
||||
configs.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelBridge {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
109
crates/zclaw-channels/src/channel.rs
Normal file
109
crates/zclaw-channels/src/channel.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
//! Channel trait and types
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zclaw_types::{Result, AgentId};
|
||||
|
||||
/// Channel configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChannelConfig {
|
||||
/// Unique channel identifier
|
||||
pub id: String,
|
||||
/// Channel type (telegram, discord, slack, etc.)
|
||||
pub channel_type: String,
|
||||
/// Human-readable name
|
||||
pub name: String,
|
||||
/// Whether the channel is enabled
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
/// Channel-specific configuration
|
||||
#[serde(default)]
|
||||
pub config: serde_json::Value,
|
||||
/// Associated agent for this channel
|
||||
pub agent_id: Option<AgentId>,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool { true }
|
||||
|
||||
/// Incoming message from a channel
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IncomingMessage {
|
||||
/// Message ID from the platform
|
||||
pub platform_id: String,
|
||||
/// Channel/conversation ID
|
||||
pub conversation_id: String,
|
||||
/// Sender information
|
||||
pub sender: MessageSender,
|
||||
/// Message content
|
||||
pub content: MessageContent,
|
||||
/// Timestamp
|
||||
pub timestamp: i64,
|
||||
/// Reply-to message ID if any
|
||||
pub reply_to: Option<String>,
|
||||
}
|
||||
|
||||
/// Message sender information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageSender {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub username: Option<String>,
|
||||
pub is_bot: bool,
|
||||
}
|
||||
|
||||
/// Message content types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum MessageContent {
|
||||
Text { text: String },
|
||||
Image { url: String, caption: Option<String> },
|
||||
File { url: String, filename: String },
|
||||
Audio { url: String },
|
||||
Video { url: String },
|
||||
Location { latitude: f64, longitude: f64 },
|
||||
Sticker { emoji: Option<String>, url: Option<String> },
|
||||
}
|
||||
|
||||
/// Outgoing message to a channel
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutgoingMessage {
|
||||
/// Conversation/channel ID to send to
|
||||
pub conversation_id: String,
|
||||
/// Message content
|
||||
pub content: MessageContent,
|
||||
/// Reply-to message ID if any
|
||||
pub reply_to: Option<String>,
|
||||
/// Whether to send silently (no notification)
|
||||
pub silent: bool,
|
||||
}
|
||||
|
||||
/// Channel connection status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ChannelStatus {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
/// Channel trait for platform adapters
|
||||
#[async_trait]
|
||||
pub trait Channel: Send + Sync {
|
||||
/// Get channel configuration
|
||||
fn config(&self) -> &ChannelConfig;
|
||||
|
||||
/// Connect to the platform
|
||||
async fn connect(&self) -> Result<()>;
|
||||
|
||||
/// Disconnect from the platform
|
||||
async fn disconnect(&self) -> Result<()>;
|
||||
|
||||
/// Get current connection status
|
||||
async fn status(&self) -> ChannelStatus;
|
||||
|
||||
/// Send a message
|
||||
async fn send(&self, message: OutgoingMessage) -> Result<String>;
|
||||
|
||||
/// Receive incoming messages (streaming)
|
||||
async fn receive(&self) -> Result<tokio::sync::mpsc::Receiver<IncomingMessage>>;
|
||||
}
|
||||
11
crates/zclaw-channels/src/lib.rs
Normal file
11
crates/zclaw-channels/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! ZCLAW Channels
|
||||
//!
|
||||
//! External platform adapters for unified message handling.
|
||||
|
||||
mod channel;
|
||||
mod bridge;
|
||||
mod adapters;
|
||||
|
||||
pub use channel::*;
|
||||
pub use bridge::*;
|
||||
pub use adapters::*;
|
||||
Reference in New Issue
Block a user