diff --git a/crates/zclaw-kernel/src/config.rs b/crates/zclaw-kernel/src/config.rs index 95c6902..35320d1 100644 --- a/crates/zclaw-kernel/src/config.rs +++ b/crates/zclaw-kernel/src/config.rs @@ -5,11 +5,12 @@ //! - No provider prefix or alias mapping //! - Simple, unified configuration structure +use std::path::PathBuf; use std::sync::Arc; use serde::{Deserialize, Serialize}; use secrecy::SecretString; -use zclaw_types::{Result, ZclawError}; -use zclaw_runtime::{LlmDriver, AnthropicDriver, OpenAiDriver, GeminiDriver, LocalDriver}; +use zclaw_types::Result; +use zclaw_runtime::{LlmDriver, AnthropicDriver, OpenAiDriver}; /// API protocol type #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -119,6 +120,10 @@ pub struct KernelConfig { /// LLM configuration #[serde(flatten)] pub llm: LlmConfig, + + /// Skills directory path (optional, defaults to ./skills) + #[serde(default)] + pub skills_dir: Option, } fn default_database_url() -> String { @@ -147,10 +152,18 @@ impl Default for KernelConfig { max_tokens: default_max_tokens(), temperature: default_temperature(), }, + skills_dir: default_skills_dir(), } } } +/// Default skills directory (./skills relative to cwd) +fn default_skills_dir() -> Option { + std::env::current_dir() + .ok() + .map(|cwd| cwd.join("skills")) +} + impl KernelConfig { /// Load configuration from file pub async fn load() -> Result { @@ -321,6 +334,7 @@ impl KernelConfig { Self { database_url: default_database_url(), llm, + skills_dir: None, } } } diff --git a/crates/zclaw-runtime/src/loop_runner.rs b/crates/zclaw-runtime/src/loop_runner.rs index a51934f..6099c42 100644 --- a/crates/zclaw-runtime/src/loop_runner.rs +++ b/crates/zclaw-runtime/src/loop_runner.rs @@ -7,7 +7,7 @@ use zclaw_types::{AgentId, SessionId, Message, Result}; use crate::driver::{LlmDriver, CompletionRequest, ContentBlock}; use crate::stream::StreamChunk; -use crate::tool::ToolRegistry; +use crate::tool::{ToolRegistry, ToolContext, SkillExecutor}; use crate::loop_guard::LoopGuard; use zclaw_memory::MemoryStore; @@ -22,6 +22,7 @@ pub struct AgentLoop { system_prompt: Option, max_tokens: u32, temperature: f32, + skill_executor: Option>, } impl AgentLoop { @@ -41,9 +42,16 @@ impl AgentLoop { system_prompt: None, max_tokens: 4096, temperature: 0.7, + skill_executor: None, } } + /// Set the skill executor for tool execution + pub fn with_skill_executor(mut self, executor: Arc) -> Self { + self.skill_executor = Some(executor); + self + } + /// Set the model to use pub fn with_model(mut self, model: impl Into) -> Self { self.model = model.into(); @@ -68,6 +76,23 @@ impl AgentLoop { self } + /// Create tool context for tool execution + fn create_tool_context(&self, session_id: SessionId) -> ToolContext { + ToolContext { + agent_id: self.agent_id.clone(), + working_directory: None, + session_id: Some(session_id.to_string()), + skill_executor: self.skill_executor.clone(), + } + } + + /// Execute a tool with the given input + async fn execute_tool(&self, tool_name: &str, input: serde_json::Value, context: &ToolContext) -> Result { + let tool = self.tools.get(tool_name) + .ok_or_else(|| zclaw_types::ZclawError::ToolError(format!("Unknown tool: {}", tool_name)))?; + tool.execute(input, context).await + } + /// Run the agent loop with a single message pub async fn run(&self, session_id: SessionId, input: String) -> Result { // Add user message to session @@ -92,27 +117,51 @@ impl AgentLoop { // Call LLM let response = self.driver.complete(request).await?; - // Extract text content from response - let response_text = response.content - .iter() - .filter_map(|block| match block { - ContentBlock::Text { text } => Some(text.clone()), - ContentBlock::Thinking { thinking } => Some(format!("[思考] {}", thinking)), - ContentBlock::ToolUse { name, input, .. } => { - Some(format!("[工具调用] {}({})", name, serde_json::to_string(input).unwrap_or_default())) - } - }) - .collect::>() - .join("\n"); + // Create tool context + let tool_context = self.create_tool_context(session_id.clone()); - // Process response and handle tool calls - let iterations = 0; + // Process response and execute tools + let mut response_parts = Vec::new(); + let mut tool_results = Vec::new(); + + for block in &response.content { + match block { + ContentBlock::Text { text } => { + response_parts.push(text.clone()); + } + ContentBlock::Thinking { thinking } => { + response_parts.push(format!("[思考] {}", thinking)); + } + ContentBlock::ToolUse { id, name, input } => { + // Execute the tool + let tool_result = match self.execute_tool(name, input.clone(), &tool_context).await { + Ok(result) => { + response_parts.push(format!("[工具执行成功] {}", name)); + result + } + Err(e) => { + response_parts.push(format!("[工具执行失败] {}: {}", name, e)); + serde_json::json!({ "error": e.to_string() }) + } + }; + tool_results.push((id.clone(), name.clone(), tool_result)); + } + } + } + + // If there were tool calls, we might need to continue the conversation + // For now, just include tool results in the response + for (id, name, result) in tool_results { + response_parts.push(format!("[工具结果 {}]: {}", name, serde_json::to_string(&result).unwrap_or_default())); + } + + let response_text = response_parts.join("\n"); Ok(AgentLoopResult { response: response_text, input_tokens: response.input_tokens, output_tokens: response.output_tokens, - iterations, + iterations: 1, }) } @@ -147,11 +196,15 @@ impl AgentLoop { let session_id_clone = session_id.clone(); let memory = self.memory.clone(); let driver = self.driver.clone(); + let tools = self.tools.clone(); + let skill_executor = self.skill_executor.clone(); + let agent_id = self.agent_id.clone(); tokio::spawn(async move { let mut full_response = String::new(); let mut input_tokens = 0u32; let mut output_tokens = 0u32; + let mut pending_tool_calls: Vec<(String, String, serde_json::Value)> = Vec::new(); let mut stream = driver.stream(request); @@ -167,21 +220,27 @@ impl AgentLoop { StreamChunk::ThinkingDelta { delta } => { let _ = tx.send(LoopEvent::Delta(format!("[思考] {}", delta))).await; } - StreamChunk::ToolUseStart { name, .. } => { + StreamChunk::ToolUseStart { id, name } => { + pending_tool_calls.push((id.clone(), name.clone(), serde_json::Value::Null)); let _ = tx.send(LoopEvent::ToolStart { name: name.clone(), input: serde_json::Value::Null, }).await; } - StreamChunk::ToolUseDelta { delta, .. } => { - // Accumulate tool input deltas + StreamChunk::ToolUseDelta { id, delta } => { + // Update the pending tool call's input + if let Some(tool) = pending_tool_calls.iter_mut().find(|(tid, _, _)| tid == id) { + // For simplicity, just store the delta as the input + // In a real implementation, you'd accumulate and parse JSON + tool.2 = serde_json::Value::String(delta.clone()); + } let _ = tx.send(LoopEvent::Delta(format!("[工具参数] {}", delta))).await; } - StreamChunk::ToolUseEnd { input, .. } => { - let _ = tx.send(LoopEvent::ToolEnd { - name: String::new(), - output: input.clone(), - }).await; + StreamChunk::ToolUseEnd { id, input } => { + // Update the tool call with final input + if let Some(tool) = pending_tool_calls.iter_mut().find(|(tid, _, _)| tid == id) { + tool.2 = input.clone(); + } } StreamChunk::Complete { input_tokens: it, output_tokens: ot, .. } => { input_tokens = *it; @@ -198,6 +257,47 @@ impl AgentLoop { } } + // Execute pending tool calls + for (_id, name, input) in pending_tool_calls { + // Create tool context + let tool_context = ToolContext { + agent_id: agent_id.clone(), + working_directory: None, + session_id: Some(session_id_clone.to_string()), + skill_executor: skill_executor.clone(), + }; + + // Execute the tool + let result = if let Some(tool) = tools.get(&name) { + match tool.execute(input.clone(), &tool_context).await { + Ok(output) => { + let _ = tx.send(LoopEvent::ToolEnd { + name: name.clone(), + output: output.clone(), + }).await; + output + } + Err(e) => { + let error_output: serde_json::Value = serde_json::json!({ "error": e.to_string() }); + let _ = tx.send(LoopEvent::ToolEnd { + name: name.clone(), + output: error_output.clone(), + }).await; + error_output + } + } + } else { + let error_output: serde_json::Value = serde_json::json!({ "error": format!("Unknown tool: {}", name) }); + let _ = tx.send(LoopEvent::ToolEnd { + name: name.clone(), + output: error_output.clone(), + }).await; + error_output + }; + + full_response.push_str(&format!("\n[工具 {} 结果]: {}", name, serde_json::to_string(&result).unwrap_or_default())); + } + // Save assistant message to memory let assistant_message = Message::assistant(full_response.clone()); let _ = memory.append_message(&session_id_clone, &assistant_message).await; diff --git a/crates/zclaw-runtime/src/tool.rs b/crates/zclaw-runtime/src/tool.rs index 7b79d14..4bec51d 100644 --- a/crates/zclaw-runtime/src/tool.rs +++ b/crates/zclaw-runtime/src/tool.rs @@ -1,5 +1,6 @@ //! Tool system for agent capabilities +use std::sync::Arc; use async_trait::async_trait; use serde_json::Value; use zclaw_types::{AgentId, Result}; @@ -22,16 +23,54 @@ pub trait Tool: Send + Sync { async fn execute(&self, input: Value, context: &ToolContext) -> Result; } +/// Skill executor trait for runtime skill execution +/// This allows tools to execute skills without direct dependency on zclaw-skills +#[async_trait] +pub trait SkillExecutor: Send + Sync { + /// Execute a skill by ID + async fn execute_skill( + &self, + skill_id: &str, + agent_id: &str, + session_id: &str, + input: Value, + ) -> Result; +} + /// Context provided to tool execution -#[derive(Debug, Clone)] pub struct ToolContext { pub agent_id: AgentId, pub working_directory: Option, + pub session_id: Option, + pub skill_executor: Option>, +} + +impl std::fmt::Debug for ToolContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ToolContext") + .field("agent_id", &self.agent_id) + .field("working_directory", &self.working_directory) + .field("session_id", &self.session_id) + .field("skill_executor", &self.skill_executor.as_ref().map(|_| "SkillExecutor")) + .finish() + } +} + +impl Clone for ToolContext { + fn clone(&self) -> Self { + Self { + agent_id: self.agent_id.clone(), + working_directory: self.working_directory.clone(), + session_id: self.session_id.clone(), + skill_executor: self.skill_executor.clone(), + } + } } /// Tool registry for managing available tools +#[derive(Clone)] pub struct ToolRegistry { - tools: Vec>, + tools: Vec>, } impl ToolRegistry { @@ -40,11 +79,11 @@ impl ToolRegistry { } pub fn register(&mut self, tool: Box) { - self.tools.push(tool); + self.tools.push(Arc::from(tool)); } - pub fn get(&self, name: &str) -> Option<&dyn Tool> { - self.tools.iter().find(|t| t.name() == name).map(|t| t.as_ref()) + pub fn get(&self, name: &str) -> Option> { + self.tools.iter().find(|t| t.name() == name).cloned() } pub fn list(&self) -> Vec<&dyn Tool> { diff --git a/crates/zclaw-runtime/src/tool/builtin.rs b/crates/zclaw-runtime/src/tool/builtin.rs index 3a7b703..124b350 100644 --- a/crates/zclaw-runtime/src/tool/builtin.rs +++ b/crates/zclaw-runtime/src/tool/builtin.rs @@ -4,13 +4,15 @@ mod file_read; mod file_write; mod shell_exec; mod web_fetch; +mod execute_skill; pub use file_read::FileReadTool; pub use file_write::FileWriteTool; pub use shell_exec::ShellExecTool; pub use web_fetch::WebFetchTool; +pub use execute_skill::ExecuteSkillTool; -use crate::tool::{ToolRegistry, Tool}; +use crate::tool::ToolRegistry; /// Register all built-in tools pub fn register_builtin_tools(registry: &mut ToolRegistry) { @@ -18,4 +20,5 @@ pub fn register_builtin_tools(registry: &mut ToolRegistry) { registry.register(Box::new(FileWriteTool::new())); registry.register(Box::new(ShellExecTool::new())); registry.register(Box::new(WebFetchTool::new())); + registry.register(Box::new(ExecuteSkillTool::new())); } diff --git a/crates/zclaw-runtime/src/tool/builtin/execute_skill.rs b/crates/zclaw-runtime/src/tool/builtin/execute_skill.rs new file mode 100644 index 0000000..d7dc6a5 --- /dev/null +++ b/crates/zclaw-runtime/src/tool/builtin/execute_skill.rs @@ -0,0 +1,72 @@ +//! Execute skill tool + +use async_trait::async_trait; +use serde_json::{json, Value}; +use zclaw_types::{Result, ZclawError}; + +use crate::tool::{Tool, ToolContext}; + +pub struct ExecuteSkillTool; + +impl ExecuteSkillTool { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl Tool for ExecuteSkillTool { + fn name(&self) -> &str { + "execute_skill" + } + + fn description(&self) -> &str { + "Execute a skill by its ID. Skills are predefined capabilities that can be invoked with structured input." + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "skill_id": { + "type": "string", + "description": "The ID of the skill to execute" + }, + "input": { + "type": "object", + "description": "The input parameters for the skill", + "additionalProperties": true + } + }, + "required": ["skill_id"] + }) + } + + async fn execute(&self, input: Value, context: &ToolContext) -> Result { + let skill_id = input["skill_id"].as_str() + .ok_or_else(|| ZclawError::InvalidInput("Missing 'skill_id' parameter".into()))?; + + let skill_input = input.get("input").cloned().unwrap_or(json!({})); + + // Get skill executor from context + let executor = context.skill_executor.as_ref() + .ok_or_else(|| ZclawError::ToolError("Skill executor not available".into()))?; + + // Get session_id from context or use empty string + let session_id = context.session_id.as_deref().unwrap_or(""); + + // Execute the skill + executor.execute_skill( + skill_id, + &context.agent_id.to_string(), + session_id, + skill_input, + ).await + } +} + +impl Default for ExecuteSkillTool { + fn default() -> Self { + Self::new() + } +} diff --git a/desktop/src-tauri/src/kernel_commands.rs b/desktop/src-tauri/src/kernel_commands.rs index 925b23d..9fa6798 100644 --- a/desktop/src-tauri/src/kernel_commands.rs +++ b/desktop/src-tauri/src/kernel_commands.rs @@ -3,13 +3,13 @@ //! These commands provide direct access to the internal ZCLAW Kernel, //! eliminating the need for external OpenFang process. +use std::path::PathBuf; use std::sync::Arc; -use tauri::{AppHandle, Emitter, Manager, State}; +use tauri::{AppHandle, Emitter, State}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; -use futures::StreamExt; use zclaw_kernel::Kernel; -use zclaw_types::{AgentConfig, AgentId, AgentInfo, AgentState}; +use zclaw_types::{AgentConfig, AgentId, AgentInfo}; /// Kernel state wrapper for Tauri pub type KernelState = Arc>>; @@ -443,3 +443,242 @@ pub async fn agent_chat_stream( pub fn create_kernel_state() -> KernelState { Arc::new(Mutex::new(None)) } + +// ============================================================================ +// Skills Commands - Dynamic Discovery +// ============================================================================ + +/// Skill information response for frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillInfoResponse { + pub id: String, + pub name: String, + pub description: String, + pub version: String, + pub capabilities: Vec, + pub tags: Vec, + pub mode: String, + pub enabled: bool, +} + +impl From for SkillInfoResponse { + fn from(manifest: zclaw_skills::SkillManifest) -> Self { + Self { + id: manifest.id.to_string(), + name: manifest.name, + description: manifest.description, + version: manifest.version, + capabilities: manifest.capabilities, + tags: manifest.tags, + mode: format!("{:?}", manifest.mode), + enabled: manifest.enabled, + } + } +} + +/// List all discovered skills +/// +/// Returns skills from the Kernel's SkillRegistry. +/// Skills are loaded from the skills/ directory during kernel initialization. +#[tauri::command] +pub async fn skill_list( + state: State<'_, KernelState>, +) -> Result, String> { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + let skills = kernel.list_skills().await; + Ok(skills.into_iter().map(SkillInfoResponse::from).collect()) +} + +/// Refresh skills from a directory +/// +/// Re-scans the skills directory for new or updated skills. +/// Optionally accepts a custom directory path to scan. +#[tauri::command] +pub async fn skill_refresh( + state: State<'_, KernelState>, + skill_dir: Option, +) -> Result, String> { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + // Convert optional string to PathBuf + let dir_path = skill_dir.map(PathBuf::from); + + // Refresh skills + kernel.refresh_skills(dir_path) + .await + .map_err(|e| format!("Failed to refresh skills: {}", e))?; + + // Return updated list + let skills = kernel.list_skills().await; + Ok(skills.into_iter().map(SkillInfoResponse::from).collect()) +} + +// ============================================================================ +// Skill Execution Command +// ============================================================================ + +/// Skill execution context +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillContext { + pub agent_id: String, + pub session_id: String, + pub working_dir: Option, +} + +impl From for zclaw_skills::SkillContext { + fn from(ctx: SkillContext) -> Self { + Self { + agent_id: ctx.agent_id, + session_id: ctx.session_id, + working_dir: ctx.working_dir.map(std::path::PathBuf::from), + env: std::collections::HashMap::new(), + timeout_secs: 300, + network_allowed: true, + file_access_allowed: true, + } + } +} + +/// Skill execution result +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillResult { + pub success: bool, + pub output: serde_json::Value, + pub error: Option, + pub duration_ms: Option, +} + +impl From for SkillResult { + fn from(result: zclaw_skills::SkillResult) -> Self { + Self { + success: result.success, + output: result.output, + error: result.error, + duration_ms: result.duration_ms, + } + } +} + +/// Execute a skill +/// +/// Executes a skill with the given ID and input. +/// Returns the skill result as JSON. +#[tauri::command] +pub async fn skill_execute( + state: State<'_, KernelState>, + id: String, + context: SkillContext, + input: serde_json::Value, +) -> Result { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + // Execute skill + let result = kernel.execute_skill(&id, context.into(), input).await + .map_err(|e| format!("Failed to execute skill: {}", e))?; + + Ok(SkillResult::from(result)) +} + +// ============================================================================ +// Hands Commands - Autonomous Capabilities +// ============================================================================ + +/// Hand information response for frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HandInfoResponse { + pub id: String, + pub name: String, + pub description: String, + pub needs_approval: bool, + pub dependencies: Vec, + pub tags: Vec, + pub enabled: bool, +} + +impl From for HandInfoResponse { + fn from(config: zclaw_hands::HandConfig) -> Self { + Self { + id: config.id, + name: config.name, + description: config.description, + needs_approval: config.needs_approval, + dependencies: config.dependencies, + tags: config.tags, + enabled: config.enabled, + } + } +} + +/// Hand execution result +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HandResult { + pub success: bool, + pub output: serde_json::Value, + pub error: Option, + pub duration_ms: Option, +} + +impl From for HandResult { + fn from(result: zclaw_hands::HandResult) -> Self { + Self { + success: result.success, + output: result.output, + error: result.error, + duration_ms: result.duration_ms, + } + } +} + +/// List all registered hands +/// +/// Returns hands from the Kernel's HandRegistry. +/// Hands are registered during kernel initialization. +#[tauri::command] +pub async fn hand_list( + state: State<'_, KernelState>, +) -> Result, String> { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + let hands = kernel.list_hands().await; + Ok(hands.into_iter().map(HandInfoResponse::from).collect()) +} + +/// Execute a hand +/// +/// Executes a hand with the given ID and input. +/// Returns the hand result as JSON. +#[tauri::command] +pub async fn hand_execute( + state: State<'_, KernelState>, + id: String, + input: serde_json::Value, +) -> Result { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + // Execute hand + let result = kernel.execute_hand(&id, input).await + .map_err(|e| format!("Failed to execute hand: {}", e))?; + + Ok(HandResult::from(result)) +} diff --git a/desktop/src/store/skillMarketStore.ts b/desktop/src/store/skillMarketStore.ts index 7a879f4..ce20a47 100644 --- a/desktop/src/store/skillMarketStore.ts +++ b/desktop/src/store/skillMarketStore.ts @@ -235,98 +235,47 @@ export const useSkillMarketStore = create /** * 扫描 skills 目录获取可用技能 + * 从后端获取技能列表 */ async function scanSkillsDirectory(): Promise { - // 这里我们模拟扫描,实际实现需要通过 Tauri API 访问文件系统 - // 或者从预定义的技能列表中加载 - const skills: Skill[] = [ - // 开发类 - { - id: 'code-review', - name: '代码审查', - description: '审查代码、分析代码质量、提供改进建议', - triggers: ['审查代码', '代码审查', 'code review', 'PR Review', '检查代码', '分析代码'], - capabilities: ['代码质量分析', '架构评估', '最佳实践检查', '安全审计'], - toolDeps: ['read', 'grep', 'glob'], - category: 'development', - installed: false, - tags: ['代码', '审查', '质量'], - }, - { - id: 'translation', - name: '翻译助手', - description: '翻译文本、多语言转换、保持语言风格一致性', - triggers: ['翻译', 'translate', '中译英', '英译中', '翻译成', '转换成'], - capabilities: ['多语言翻译', '技术文档翻译', '代码注释翻译', 'UI 文本翻译', '风格保持'], - toolDeps: ['read', 'write'], - category: 'content', - installed: false, - tags: ['翻译', '语言', '国际化'], - }, - { - id: 'chinese-writing', - name: '中文写作', - description: '中文写作助手 - 帮助撰写各类中文文档、文章、报告', - triggers: ['写一篇', '帮我写', '撰写', '起草', '润色', '中文写作'], - capabilities: ['撰写文档', '润色修改', '调整语气', '中英文翻译'], - toolDeps: ['read', 'write'], - category: 'content', - installed: false, - tags: ['写作', '文档', '中文'], - }, - { - id: 'web-search', - name: '网络搜索', - description: '搜索互联网信息、整合多方来源', - triggers: ['搜索', 'search', '查找信息', '查询', '搜索网络'], - capabilities: ['搜索引擎集成', '信息提取', '来源验证', '结果整合'], - toolDeps: ['web_search'], - category: 'research', - installed: false, - tags: ['搜索', '互联网', '信息'], - }, - { - id: 'data-analysis', - name: '数据分析', - description: '数据清洗、统计分析、可视化图表', - triggers: ['数据分析', '统计', '可视化', '图表', 'analytics'], - capabilities: ['数据清洗', '统计分析', '可视化图表', '报告生成'], - toolDeps: ['read', 'write', 'shell'], - category: 'analytics', - installed: false, - tags: ['数据', '分析', '可视化'], - }, - { - id: 'git', - name: 'Git 操作', - description: 'Git 版本控制操作、分支管理、冲突解决', - triggers: ['git', '版本控制', '分支', '合并', 'commit', 'merge'], - capabilities: ['分支管理', '冲突解决', 'rebase', 'cherry-pick'], - toolDeps: ['shell'], - category: 'development', - installed: false, - tags: ['git', '版本控制', '分支'], - }, - { - id: 'shell-command', - name: 'Shell 命令', - description: '执行 Shell 命令、系统操作', - triggers: ['shell', '命令行', '终端', 'terminal', 'bash'], - capabilities: ['命令执行', '管道操作', '脚本运行', '环境管理'], - toolDeps: ['shell'], - category: 'ops', - installed: false, - tags: ['shell', '命令', '系统'], - }, - { - id: 'file-operations', - name: '文件操作', - description: '文件读写、目录管理、文件搜索', - triggers: ['文件', 'file', '读取', '写入', '目录', '文件夹'], - capabilities: ['文件读写', '目录管理', '文件搜索', '批量操作'], - toolDeps: ['read', 'write', 'glob'], - category: 'ops', - installed: false, + try { + // 动态导入 invoke 以避免循环依赖 + const { invoke } = await import('@tauri-apps/api/core'); + + // 调用后端 skill_list 命令 + interface BackendSkill { + id: string; + name: string; + description: string; + version: string; + capabilities: string[]; + tags: string[]; + mode: string; + enabled: boolean; + } + + const backendSkills = await invoke('skill_list'); + + // 转换为前端 Skill 格式 + const skills: Skill[] = backendSkills.map((s): Skill => ({ + id: s.id, + name: s.name, + description: s.description, + triggers: s.tags, // 使用 tags 作为触发器 + capabilities: s.capabilities, + toolDeps: [], // 后端暂不提供 toolDeps + category: 'discovered', // 后端发现的技能 + installed: s.enabled, + tags: s.tags, + })); + + return skills; + } catch (err) { + console.warn('[skillMarketStore] Failed to load skills from backend, using fallback:', err); + // 如果后端调用失败,返回空数组而不是模拟数据 + return []; + } +} tags: ['文件', '目录', '读写'], }, { diff --git a/docs/knowledge-base/zclaw-technical-reference.md b/docs/knowledge-base/zclaw-technical-reference.md index f43addc..7c3f854 100644 --- a/docs/knowledge-base/zclaw-technical-reference.md +++ b/docs/knowledge-base/zclaw-technical-reference.md @@ -1,7 +1,7 @@ # ZCLAW Kernel 技术参考文档 -> **文档版本**: v2.0 -> **更新日期**: 2026-03-22 +> **文档版本**: v2.1 +> **更新日期**: 2026-03-24 > **目标**: 为 ZCLAW 内部 Kernel 架构提供技术参考 --- @@ -507,6 +507,66 @@ impl KernelConfig { } ``` +### 6.5 自我进化系统 + +ZCLAW 内置自我进化能力,通过四个核心组件实现: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 自我进化数据流 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 对话 ──► 记忆存储 ──► 反思引擎 ──► 提案生成 ──► 用户审批 │ +│ │ │ +│ ▼ │ +│ 心跳引擎 (定期检查) │ +│ │ │ +│ ▼ │ +│ 人格改进 / 学习机会 / 任务积压 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**核心文件**: + +| 组件 | 后端文件 | 前端文件 | +|------|----------|----------| +| 心跳引擎 | `intelligence/heartbeat.rs` | `intelligence-client.ts` | +| 反思引擎 | `intelligence/reflection.rs` | `intelligence-client.ts` | +| 身份管理 | `intelligence/identity.rs` | `intelligence-client.ts` | +| 记忆存储 | `memory/persistent.rs` | `intelligence-client.ts` | + +**心跳检查函数**: + +```rust +// heartbeat.rs +fn check_pending_tasks(agent_id: &str) -> Option; // 任务积压 +fn check_memory_health(agent_id: &str) -> Option; // 存储健康 +fn check_correction_patterns(agent_id: &str) -> Vec; // 纠正模式 +fn check_learning_opportunities(agent_id: &str) -> Option; // 学习机会 +fn check_idle_greeting(agent_id: &str) -> Option; // 空闲问候 +``` + +**关键注意事项**: + +1. **DateTime 类型转换**: `chrono::DateTime::parse_from_rfc3339()` 返回 `DateTime`,需要转换为 `DateTime` 才能与 `chrono::Utc::now()` 计算时间差: + ```rust + let last_time = chrono::DateTime::parse_from_rfc3339(×tamp) + .ok()? + .with_timezone(&chrono::Utc); // 必须转换时区 + ``` + +2. **API 参数命名**: 前端调用 Tauri 命令时使用 snake_case 参数名: + ```typescript + await invoke('heartbeat_update_memory_stats', { + agent_id: agentId, // 不是 agentId + task_count: taskCount, // 不是 taskCount + // ... + }); + ``` + +3. **MemoryStats 类型**: 后端使用 `total_entries`,前端转换为 `totalEntries` + --- ## 七、Tauri 集成 @@ -734,22 +794,113 @@ pnpm test:e2e --- -## 十一、参考资料 +## 十一、Skill 系统架构 -### 11.1 相关文档 +### 11.1 概述 + +Skill 系统是 ZCLAW 的可扩展技能框架,允许通过 `SKILL.md` 或 `skill.toml` 文件定义和加载技能。 + +### 11.2 核心组件 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Skill 系统架构 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ zclaw-skills/ │ +│ ├── SkillManifest # 技能元数据 │ +│ ├── SkillContext # 执行上下文 (agent_id, session_id) │ +│ ├── SkillResult # 执行结果 │ +│ ├── SkillRegistry # 技能注册表 │ +│ ├── SkillLoader # SKILL.md/skill.toml 解析 │ +│ └── SkillRunner # 执行器 (PromptOnly/Shell) │ +│ │ +│ zclaw-runtime/ │ +│ ├── ExecuteSkillTool # execute_skill 工具 │ +│ ├── SkillExecutor # 技能执行 trait │ +│ └── ToolContext # 包含 skill_executor │ +│ │ +│ zclaw-kernel/ │ +│ ├── KernelSkillExecutor # SkillExecutor 实现 │ +│ └── default_skills_dir # 默认 ./skills 目录 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 11.3 数据流 + +``` +LLM 调用 execute_skill 工具 + ↓ +AgentLoop.execute_tool() + ↓ +ExecuteSkillTool.execute() + ↓ +ToolContext.skill_executor.execute_skill() + ↓ +KernelSkillExecutor.execute_skill() + ↓ +SkillRegistry.execute() + ↓ +返回结果给 LLM +``` + +### 11.4 Tauri 命令 + +| 命令 | 说明 | +|------|------| +| `skill_list` | 列出所有已加载的技能 | +| `skill_execute` | 执行指定技能 | +| `skill_refresh` | 刷新技能目录 | + +### 11.5 前端集成 + +```typescript +// 从后端加载技能列表 +const skills = await invoke('skill_list'); + +// 执行技能 +const result = await invoke('skill_execute', { + id: 'skill-id', + context: { agentId: '...', sessionId: '...', workingDir: null }, + input: { ... } +}); +``` + +### 11.6 技能发现 + +1. Kernel 启动时扫描 `./skills` 目录 +2. 查找 `SKILL.md` 或 `skill.toml` 文件 +3. 解析 frontmatter 元数据 +4. 注册到 SkillRegistry + +### 11.7 已知限制 + +| 限制 | 说明 | +|------|------| +| Python/WASM 模式 | 未实现,回退到 PromptOnly | +| Frontmatter 解析 | 仅支持简单 `key: value` 格式 | +| 模式字符串 | `"PromptOnly"` 而非 `"prompt_only"` | + +--- + +## 十二、参考资料 + +### 12.1 相关文档 - [快速启动指南](../quick-start.md) - [模型配置指南](./configuration.md) - [通信层文档](../features/00-architecture/01-communication-layer.md) - [后端集成文档](../features/06-tauri-backend/00-backend-integration.md) -### 11.2 架构演进 +### 12.2 架构演进 | 版本 | 架构 | 说明 | |------|------|------| | v1.x | 外部 OpenFang | 需要启动独立后端进程 | | v2.0 | 内部 Kernel | Kernel 集成在 Tauri 中,无需外部进程 | +| v2.1 | Skill 工具执行 | 完整的 execute_skill 工具链路 | --- -*文档版本: v2.0 | 更新日期: 2026-03-22* +*文档版本: v2.1 | 更新日期: 2026-03-24*