feat(hands): implement 4 new Hands and fix BrowserHand registration
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- Add ResearcherHand: DuckDuckGo search, web fetch, report generation
- Add CollectorHand: data collection, aggregation, multiple output formats
- Add ClipHand: video processing (trim, convert, thumbnail, concat)
- Add TwitterHand: Twitter/X automation (tweet, retweet, like, search)
- Fix BrowserHand not registered in Kernel (critical bug)
- Add HandError variant to ZclawError enum
- Update documentation: 9/11 Hands implemented (82%)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-24 13:22:44 +08:00
parent 3ff08faa56
commit 1441f98c5e
15 changed files with 2376 additions and 36 deletions

View File

@@ -3,13 +3,47 @@
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use zclaw_types::{AgentConfig, AgentId, AgentInfo, Event, Result};
use async_trait::async_trait;
use serde_json::Value;
use crate::registry::AgentRegistry;
use crate::capabilities::CapabilityManager;
use crate::events::EventBus;
use crate::config::KernelConfig;
use zclaw_memory::MemoryStore;
use zclaw_runtime::{AgentLoop, LlmDriver, ToolRegistry};
use zclaw_runtime::{AgentLoop, LlmDriver, ToolRegistry, tool::SkillExecutor};
use zclaw_skills::SkillRegistry;
use zclaw_hands::{HandRegistry, HandContext, HandResult, hands::{BrowserHand, SlideshowHand, SpeechHand, QuizHand, WhiteboardHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand}};
/// Skill executor implementation for Kernel
pub struct KernelSkillExecutor {
skills: Arc<SkillRegistry>,
}
impl KernelSkillExecutor {
pub fn new(skills: Arc<SkillRegistry>) -> Self {
Self { skills }
}
}
#[async_trait]
impl SkillExecutor for KernelSkillExecutor {
async fn execute_skill(
&self,
skill_id: &str,
agent_id: &str,
session_id: &str,
input: Value,
) -> Result<Value> {
let context = zclaw_skills::SkillContext {
agent_id: agent_id.to_string(),
session_id: session_id.to_string(),
..Default::default()
};
let result = self.skills.execute(&zclaw_types::SkillId::new(skill_id), &context, input).await?;
Ok(result.output)
}
}
/// The ZCLAW Kernel
pub struct Kernel {
@@ -19,6 +53,9 @@ pub struct Kernel {
events: EventBus,
memory: Arc<MemoryStore>,
driver: Arc<dyn LlmDriver>,
skills: Arc<SkillRegistry>,
skill_executor: Arc<KernelSkillExecutor>,
hands: Arc<HandRegistry>,
}
impl Kernel {
@@ -35,6 +72,31 @@ impl Kernel {
let capabilities = CapabilityManager::new();
let events = EventBus::new();
// Initialize skill registry
let skills = Arc::new(SkillRegistry::new());
// Scan skills directory if configured
if let Some(ref skills_dir) = config.skills_dir {
if skills_dir.exists() {
skills.add_skill_dir(skills_dir.clone()).await?;
}
}
// Initialize hand registry with built-in hands
let hands = Arc::new(HandRegistry::new());
hands.register(Arc::new(BrowserHand::new())).await;
hands.register(Arc::new(SlideshowHand::new())).await;
hands.register(Arc::new(SpeechHand::new())).await;
hands.register(Arc::new(QuizHand::new())).await;
hands.register(Arc::new(WhiteboardHand::new())).await;
hands.register(Arc::new(ResearcherHand::new())).await;
hands.register(Arc::new(CollectorHand::new())).await;
hands.register(Arc::new(ClipHand::new())).await;
hands.register(Arc::new(TwitterHand::new())).await;
// Create skill executor
let skill_executor = Arc::new(KernelSkillExecutor::new(skills.clone()));
// Restore persisted agents
let persisted = memory.list_agents().await?;
for agent in persisted {
@@ -48,6 +110,9 @@ impl Kernel {
events,
memory,
driver,
skills,
skill_executor,
hands,
})
}
@@ -128,6 +193,7 @@ impl Kernel {
self.memory.clone(),
)
.with_model(&model)
.with_skill_executor(self.skill_executor.clone())
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
@@ -173,6 +239,7 @@ impl Kernel {
self.memory.clone(),
)
.with_model(&model)
.with_skill_executor(self.skill_executor.clone())
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
@@ -202,6 +269,57 @@ impl Kernel {
pub fn config(&self) -> &KernelConfig {
&self.config
}
/// Get the skills registry
pub fn skills(&self) -> &Arc<SkillRegistry> {
&self.skills
}
/// List all discovered skills
pub async fn list_skills(&self) -> Vec<zclaw_skills::SkillManifest> {
self.skills.list().await
}
/// Refresh skills from a directory
pub async fn refresh_skills(&self, dir: Option<std::path::PathBuf>) -> Result<()> {
if let Some(path) = dir {
self.skills.add_skill_dir(path).await?;
} else if let Some(ref skills_dir) = self.config.skills_dir {
self.skills.add_skill_dir(skills_dir.clone()).await?;
}
Ok(())
}
/// Execute a skill with the given ID and input
pub async fn execute_skill(
&self,
id: &str,
context: zclaw_skills::SkillContext,
input: serde_json::Value,
) -> Result<zclaw_skills::SkillResult> {
self.skills.execute(&zclaw_types::SkillId::new(id), &context, input).await
}
/// Get the hands registry
pub fn hands(&self) -> &Arc<HandRegistry> {
&self.hands
}
/// List all registered hands
pub async fn list_hands(&self) -> Vec<zclaw_hands::HandConfig> {
self.hands.list().await
}
/// Execute a hand with the given input
pub async fn execute_hand(
&self,
hand_id: &str,
input: serde_json::Value,
) -> Result<HandResult> {
// Use default context (agent_id will be generated)
let context = HandContext::default();
self.hands.execute(hand_id, &context, input).await
}
}
/// Response from sending a message