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
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:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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: ['文件', '目录', '读写'],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user