feat(phase4): complete zclaw-skills, zclaw-hands, zclaw-channels, zclaw-protocols 模块实现

This commit is contained in:
iven
2026-03-22 08:57:37 +08:00
parent 7abfca9d5c
commit 0ab2f7afda
24 changed files with 2060 additions and 0 deletions

View 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
}
}

View 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::*;

View 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()
}
}

View 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);
}