feat(phase4): complete zclaw-skills, zclaw-hands, zclaw-channels, zclaw-protocols 模块实现
This commit is contained in:
156
crates/zclaw-hands/src/hand.rs
Normal file
156
crates/zclaw-hands/src/hand.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
//! Hand definition and types
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use zclaw_types::{Result, AgentId};
|
||||
|
||||
/// Hand configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HandConfig {
|
||||
/// Unique hand identifier
|
||||
pub id: String,
|
||||
/// Human-readable name
|
||||
pub name: String,
|
||||
/// Hand description
|
||||
pub description: String,
|
||||
/// Whether this hand needs approval before execution
|
||||
#[serde(default)]
|
||||
pub needs_approval: bool,
|
||||
/// Required dependencies
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<String>,
|
||||
/// Input schema
|
||||
#[serde(default)]
|
||||
pub input_schema: Option<Value>,
|
||||
/// Tags for categorization
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
/// Whether the hand is enabled
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool { true }
|
||||
|
||||
/// Hand execution context
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HandContext {
|
||||
/// Agent ID executing the hand
|
||||
pub agent_id: AgentId,
|
||||
/// Working directory
|
||||
pub working_dir: Option<std::path::PathBuf>,
|
||||
/// Environment variables
|
||||
pub env: std::collections::HashMap<String, String>,
|
||||
/// Timeout in seconds
|
||||
pub timeout_secs: u64,
|
||||
/// Callback URL for async results
|
||||
pub callback_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for HandContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
agent_id: AgentId::new(),
|
||||
working_dir: None,
|
||||
env: std::collections::HashMap::new(),
|
||||
timeout_secs: 300,
|
||||
callback_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hand execution result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HandResult {
|
||||
/// Whether execution succeeded
|
||||
pub success: bool,
|
||||
/// Output data
|
||||
pub output: Value,
|
||||
/// Error message if failed
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
/// Execution duration in milliseconds
|
||||
#[serde(default)]
|
||||
pub duration_ms: Option<u64>,
|
||||
/// Status message
|
||||
#[serde(default)]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl HandResult {
|
||||
pub fn success(output: Value) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
output,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
status: "completed".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
output: Value::Null,
|
||||
error: Some(message.into()),
|
||||
duration_ms: None,
|
||||
status: "failed".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pending(status: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
output: Value::Null,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
status: status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hand execution status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HandStatus {
|
||||
Idle,
|
||||
Running,
|
||||
PendingApproval,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Hand trait - autonomous capability
|
||||
#[async_trait]
|
||||
pub trait Hand: Send + Sync {
|
||||
/// Get the hand configuration
|
||||
fn config(&self) -> &HandConfig;
|
||||
|
||||
/// Execute the hand
|
||||
async fn execute(&self, context: &HandContext, input: Value) -> Result<HandResult>;
|
||||
|
||||
/// Check if the hand needs approval
|
||||
fn needs_approval(&self) -> bool {
|
||||
self.config().needs_approval
|
||||
}
|
||||
|
||||
/// Check dependencies
|
||||
fn check_dependencies(&self) -> Result<Vec<String>> {
|
||||
let missing: Vec<String> = self.config().dependencies.iter()
|
||||
.filter(|dep| !self.is_dependency_available(dep))
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(missing)
|
||||
}
|
||||
|
||||
/// Check if a specific dependency is available
|
||||
fn is_dependency_available(&self, _dep: &str) -> bool {
|
||||
true // Default implementation
|
||||
}
|
||||
|
||||
/// Get current status
|
||||
fn status(&self) -> HandStatus {
|
||||
HandStatus::Idle
|
||||
}
|
||||
}
|
||||
11
crates/zclaw-hands/src/lib.rs
Normal file
11
crates/zclaw-hands/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! ZCLAW Hands
|
||||
//!
|
||||
//! Autonomous capabilities for ZCLAW agents.
|
||||
|
||||
mod hand;
|
||||
mod registry;
|
||||
mod trigger;
|
||||
|
||||
pub use hand::*;
|
||||
pub use registry::*;
|
||||
pub use trigger::*;
|
||||
131
crates/zclaw-hands/src/registry.rs
Normal file
131
crates/zclaw-hands/src/registry.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! Hand and Trigger registries
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use super::{Hand, HandConfig, HandContext, HandResult, Trigger, TriggerConfig};
|
||||
|
||||
/// Hand registry
|
||||
pub struct HandRegistry {
|
||||
hands: RwLock<HashMap<String, Arc<dyn Hand>>>,
|
||||
configs: RwLock<HashMap<String, HandConfig>>,
|
||||
}
|
||||
|
||||
impl HandRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hands: RwLock::new(HashMap::new()),
|
||||
configs: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a hand
|
||||
pub async fn register(&self, hand: Arc<dyn Hand>) {
|
||||
let config = hand.config().clone();
|
||||
let mut hands = self.hands.write().await;
|
||||
let mut configs = self.configs.write().await;
|
||||
|
||||
hands.insert(config.id.clone(), hand);
|
||||
configs.insert(config.id.clone(), config);
|
||||
}
|
||||
|
||||
/// Get a hand by ID
|
||||
pub async fn get(&self, id: &str) -> Option<Arc<dyn Hand>> {
|
||||
let hands = self.hands.read().await;
|
||||
hands.get(id).cloned()
|
||||
}
|
||||
|
||||
/// Get hand configuration
|
||||
pub async fn get_config(&self, id: &str) -> Option<HandConfig> {
|
||||
let configs = self.configs.read().await;
|
||||
configs.get(id).cloned()
|
||||
}
|
||||
|
||||
/// List all hands
|
||||
pub async fn list(&self) -> Vec<HandConfig> {
|
||||
let configs = self.configs.read().await;
|
||||
configs.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Execute a hand
|
||||
pub async fn execute(
|
||||
&self,
|
||||
id: &str,
|
||||
context: &HandContext,
|
||||
input: serde_json::Value,
|
||||
) -> Result<HandResult> {
|
||||
let hand = self.get(id).await
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Hand not found: {}", id)))?;
|
||||
|
||||
hand.execute(context, input).await
|
||||
}
|
||||
|
||||
/// Remove a hand
|
||||
pub async fn remove(&self, id: &str) {
|
||||
let mut hands = self.hands.write().await;
|
||||
let mut configs = self.configs.write().await;
|
||||
|
||||
hands.remove(id);
|
||||
configs.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HandRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger registry
|
||||
pub struct TriggerRegistry {
|
||||
triggers: RwLock<HashMap<String, Arc<dyn Trigger>>>,
|
||||
configs: RwLock<HashMap<String, TriggerConfig>>,
|
||||
}
|
||||
|
||||
impl TriggerRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
triggers: RwLock::new(HashMap::new()),
|
||||
configs: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a trigger
|
||||
pub async fn register(&self, trigger: Arc<dyn Trigger>) {
|
||||
let config = trigger.config().clone();
|
||||
let mut triggers = self.triggers.write().await;
|
||||
let mut configs = self.configs.write().await;
|
||||
|
||||
triggers.insert(config.id.clone(), trigger);
|
||||
configs.insert(config.id.clone(), config);
|
||||
}
|
||||
|
||||
/// Get a trigger by ID
|
||||
pub async fn get(&self, id: &str) -> Option<Arc<dyn Trigger>> {
|
||||
let triggers = self.triggers.read().await;
|
||||
triggers.get(id).cloned()
|
||||
}
|
||||
|
||||
/// List all triggers
|
||||
pub async fn list(&self) -> Vec<TriggerConfig> {
|
||||
let configs = self.configs.read().await;
|
||||
configs.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Remove a trigger
|
||||
pub async fn remove(&self, id: &str) {
|
||||
let mut triggers = self.triggers.write().await;
|
||||
let mut configs = self.configs.write().await;
|
||||
|
||||
triggers.remove(id);
|
||||
configs.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TriggerRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
150
crates/zclaw-hands/src/trigger.rs
Normal file
150
crates/zclaw-hands/src/trigger.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
//! Hand trigger definitions
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Trigger configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TriggerConfig {
|
||||
/// Unique trigger identifier
|
||||
pub id: String,
|
||||
/// Human-readable name
|
||||
pub name: String,
|
||||
/// Hand ID to trigger
|
||||
pub hand_id: String,
|
||||
/// Trigger type
|
||||
pub trigger_type: TriggerType,
|
||||
/// Whether the trigger is enabled
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
/// Maximum executions per hour (rate limiting)
|
||||
#[serde(default = "default_max_executions")]
|
||||
pub max_executions_per_hour: u32,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool { true }
|
||||
fn default_max_executions() -> u32 { 10 }
|
||||
|
||||
/// Trigger type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum TriggerType {
|
||||
/// Time-based trigger
|
||||
Schedule {
|
||||
/// Cron expression
|
||||
cron: String,
|
||||
},
|
||||
/// Event-based trigger
|
||||
Event {
|
||||
/// Event pattern to match
|
||||
pattern: String,
|
||||
},
|
||||
/// Webhook trigger
|
||||
Webhook {
|
||||
/// Webhook path
|
||||
path: String,
|
||||
/// Secret for verification
|
||||
secret: Option<String>,
|
||||
},
|
||||
/// Message pattern trigger
|
||||
MessagePattern {
|
||||
/// Regex pattern
|
||||
pattern: String,
|
||||
},
|
||||
/// File system trigger
|
||||
FileSystem {
|
||||
/// Path to watch
|
||||
path: String,
|
||||
/// Events to watch for
|
||||
events: Vec<FileEvent>,
|
||||
},
|
||||
/// Manual trigger only
|
||||
Manual,
|
||||
}
|
||||
|
||||
/// File system event types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FileEvent {
|
||||
Created,
|
||||
Modified,
|
||||
Deleted,
|
||||
Any,
|
||||
}
|
||||
|
||||
/// Trigger state
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TriggerState {
|
||||
/// Trigger ID
|
||||
pub trigger_id: String,
|
||||
/// Last execution time
|
||||
pub last_execution: Option<DateTime<Utc>>,
|
||||
/// Execution count in current hour
|
||||
pub execution_count: u32,
|
||||
/// Last execution result
|
||||
pub last_result: Option<TriggerResult>,
|
||||
/// Whether the trigger is active
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl TriggerState {
|
||||
pub fn new(trigger_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
trigger_id: trigger_id.into(),
|
||||
last_execution: None,
|
||||
execution_count: 0,
|
||||
last_result: None,
|
||||
is_active: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger execution result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TriggerResult {
|
||||
/// Execution timestamp
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Whether execution succeeded
|
||||
pub success: bool,
|
||||
/// Output from hand execution
|
||||
pub output: Option<Value>,
|
||||
/// Error message if failed
|
||||
pub error: Option<String>,
|
||||
/// Input that triggered execution
|
||||
pub trigger_input: Value,
|
||||
}
|
||||
|
||||
impl TriggerResult {
|
||||
pub fn success(trigger_input: Value, output: Value) -> Self {
|
||||
Self {
|
||||
timestamp: Utc::now(),
|
||||
success: true,
|
||||
output: Some(output),
|
||||
error: None,
|
||||
trigger_input,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(trigger_input: Value, error: impl Into<String>) -> Self {
|
||||
Self {
|
||||
timestamp: Utc::now(),
|
||||
success: false,
|
||||
output: None,
|
||||
error: Some(error.into()),
|
||||
trigger_input,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger trait
|
||||
pub trait Trigger: Send + Sync {
|
||||
/// Get trigger configuration
|
||||
fn config(&self) -> &TriggerConfig;
|
||||
|
||||
/// Check if trigger should fire
|
||||
fn should_fire(&self, input: &Value) -> bool;
|
||||
|
||||
/// Update trigger state
|
||||
fn update_state(&mut self, result: TriggerResult);
|
||||
}
|
||||
Reference in New Issue
Block a user