feat(skill-execution): implement execute_skill tool with full execution chain
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 ExecuteSkillTool for LLM to call skills during conversation
- Implement SkillExecutor trait in Kernel for skill execution
- Update AgentLoop to support tool execution with skill_executor
- Add default skills_dir configuration in KernelConfig
- Connect frontend skillMarketStore to backend skill_list command
- Update technical documentation with Skill system architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-24 13:24:23 +08:00
parent 1441f98c5e
commit 504d5746aa
8 changed files with 698 additions and 131 deletions

View File

@@ -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<PathBuf>,
}
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::path::PathBuf> {
std::env::current_dir()
.ok()
.map(|cwd| cwd.join("skills"))
}
impl KernelConfig {
/// Load configuration from file
pub async fn load() -> Result<Self> {
@@ -321,6 +334,7 @@ impl KernelConfig {
Self {
database_url: default_database_url(),
llm,
skills_dir: None,
}
}
}

View File

@@ -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<String>,
max_tokens: u32,
temperature: f32,
skill_executor: Option<Arc<dyn SkillExecutor>>,
}
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<dyn SkillExecutor>) -> Self {
self.skill_executor = Some(executor);
self
}
/// Set the model to use
pub fn with_model(mut self, model: impl Into<String>) -> 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<serde_json::Value> {
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<AgentLoopResult> {
// 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::<Vec<_>>()
.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;

View File

@@ -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<Value>;
}
/// 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<Value>;
}
/// Context provided to tool execution
#[derive(Debug, Clone)]
pub struct ToolContext {
pub agent_id: AgentId,
pub working_directory: Option<String>,
pub session_id: Option<String>,
pub skill_executor: Option<Arc<dyn SkillExecutor>>,
}
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<Box<dyn Tool>>,
tools: Vec<Arc<dyn Tool>>,
}
impl ToolRegistry {
@@ -40,11 +79,11 @@ impl ToolRegistry {
}
pub fn register(&mut self, tool: Box<dyn Tool>) {
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<Arc<dyn Tool>> {
self.tools.iter().find(|t| t.name() == name).cloned()
}
pub fn list(&self) -> Vec<&dyn Tool> {

View File

@@ -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()));
}

View File

@@ -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<Value> {
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()
}
}

View File

@@ -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<Mutex<Option<Kernel>>>;
@@ -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<String>,
pub tags: Vec<String>,
pub mode: String,
pub enabled: bool,
}
impl From<zclaw_skills::SkillManifest> 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<Vec<SkillInfoResponse>, 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<String>,
) -> Result<Vec<SkillInfoResponse>, 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<String>,
}
impl From<SkillContext> 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<String>,
pub duration_ms: Option<u64>,
}
impl From<zclaw_skills::SkillResult> 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<SkillResult, 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())?;
// 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<String>,
pub tags: Vec<String>,
pub enabled: bool,
}
impl From<zclaw_hands::HandConfig> 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<String>,
pub duration_ms: Option<u64>,
}
impl From<zclaw_hands::HandResult> 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<Vec<HandInfoResponse>, 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<HandResult, 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())?;
// Execute hand
let result = kernel.execute_hand(&id, input).await
.map_err(|e| format!("Failed to execute hand: {}", e))?;
Ok(HandResult::from(result))
}

View File

@@ -235,98 +235,47 @@ export const useSkillMarketStore = create<SkillMarketState & SkillMarketActions>
/**
* 扫描 skills 目录获取可用技能
* 从后端获取技能列表
*/
async function scanSkillsDirectory(): Promise<Skill[]> {
// 这里我们模拟扫描,实际实现需要通过 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<BackendSkill[]>('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: ['文件', '目录', '读写'],
},
{

View File

@@ -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<HeartbeatAlert>; // 任务积压
fn check_memory_health(agent_id: &str) -> Option<HeartbeatAlert>; // 存储健康
fn check_correction_patterns(agent_id: &str) -> Vec<HeartbeatAlert>; // 纠正模式
fn check_learning_opportunities(agent_id: &str) -> Option<HeartbeatAlert>; // 学习机会
fn check_idle_greeting(agent_id: &str) -> Option<HeartbeatAlert>; // 空闲问候
```
**关键注意事项**:
1. **DateTime 类型转换**: `chrono::DateTime::parse_from_rfc3339()` 返回 `DateTime<FixedOffset>`,需要转换为 `DateTime<Utc>` 才能与 `chrono::Utc::now()` 计算时间差:
```rust
let last_time = chrono::DateTime::parse_from_rfc3339(&timestamp)
.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*