feat: 新增技能编排引擎和工作流构建器组件
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

refactor: 统一Hands系统常量到单个源文件
refactor: 更新Hands中文名称和描述

fix: 修复技能市场在连接状态变化时重新加载
fix: 修复身份变更提案的错误处理逻辑

docs: 更新多个功能文档的验证状态和实现位置
docs: 更新Hands系统文档

test: 添加测试文件验证工作区路径
This commit is contained in:
iven
2026-03-25 08:27:25 +08:00
parent 9c781f5f2a
commit aa6a9cbd84
110 changed files with 12384 additions and 1337 deletions

View File

@@ -7,7 +7,7 @@
//! Phase 2 of Intelligence Layer Migration.
//! Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.4.1
use chrono::{DateTime, Local, Timelike};
use chrono::{Local, Timelike};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
@@ -342,6 +342,10 @@ static CORRECTION_COUNTERS: OnceLock<RwLock<StdHashMap<String, usize>>> = OnceLo
/// Key: agent_id, Value: (task_count, total_memories, storage_bytes)
static MEMORY_STATS_CACHE: OnceLock<RwLock<StdHashMap<String, MemoryStatsCache>>> = OnceLock::new();
/// Global last interaction timestamps
/// Key: agent_id, Value: last interaction timestamp (RFC3339)
static LAST_INTERACTION: OnceLock<RwLock<StdHashMap<String, String>>> = OnceLock::new();
/// Cached memory stats for an agent
#[derive(Clone, Debug, Default)]
pub struct MemoryStatsCache {
@@ -359,6 +363,18 @@ fn get_memory_stats_cache() -> &'static RwLock<StdHashMap<String, MemoryStatsCac
MEMORY_STATS_CACHE.get_or_init(|| RwLock::new(StdHashMap::new()))
}
fn get_last_interaction_map() -> &'static RwLock<StdHashMap<String, String>> {
LAST_INTERACTION.get_or_init(|| RwLock::new(StdHashMap::new()))
}
/// Record an interaction for an agent (call from frontend when user sends message)
pub fn record_interaction(agent_id: &str) {
let map = get_last_interaction_map();
if let Ok(mut map) = map.write() {
map.insert(agent_id.to_string(), chrono::Utc::now().to_rfc3339());
}
}
/// Update memory stats cache for an agent
/// Call this from frontend via Tauri command after fetching memory stats
pub fn update_memory_stats_cache(agent_id: &str, task_count: usize, total_entries: usize, storage_size_bytes: usize) {
@@ -433,10 +449,10 @@ fn check_correction_patterns(agent_id: &str) -> Vec<HeartbeatAlert> {
/// Check for pending task memories
/// Uses cached memory stats to detect task backlog
fn check_pending_tasks(agent_id: &str) -> Option<HeartbeatAlert> {
if let Some(stats) = get_cached_memory_stats(agent_id) {
// Alert if there are 5+ pending tasks
if stats.task_count >= 5 {
return Some(HeartbeatAlert {
match get_cached_memory_stats(agent_id) {
Some(stats) if stats.task_count >= 5 => {
// Alert if there are 5+ pending tasks
Some(HeartbeatAlert {
title: "待办任务积压".to_string(),
content: format!("当前有 {} 个待办任务未完成,建议处理或重新评估优先级", stats.task_count),
urgency: if stats.task_count >= 10 {
@@ -446,51 +462,102 @@ fn check_pending_tasks(agent_id: &str) -> Option<HeartbeatAlert> {
},
source: "pending-tasks".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
});
})
},
Some(_) => None, // Stats available but no alert needed
None => {
// Cache is empty - warn about missing sync
tracing::warn!("[Heartbeat] Memory stats cache is empty for agent {}, waiting for frontend sync", agent_id);
Some(HeartbeatAlert {
title: "记忆统计未同步".to_string(),
content: "心跳引擎未能获取记忆统计信息,部分检查被跳过。请确保记忆系统正常运行。".to_string(),
urgency: Urgency::Low,
source: "pending-tasks".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
})
}
}
None
}
/// Check memory storage health
/// Uses cached memory stats to detect storage issues
fn check_memory_health(agent_id: &str) -> Option<HeartbeatAlert> {
if let Some(stats) = get_cached_memory_stats(agent_id) {
// Alert if storage is very large (> 50MB)
if stats.storage_size_bytes > 50 * 1024 * 1024 {
return Some(HeartbeatAlert {
title: "记忆存储过大".to_string(),
content: format!(
"记忆存储已达 {:.1}MB建议清理低重要性记忆或归档旧记忆",
stats.storage_size_bytes as f64 / (1024.0 * 1024.0)
),
urgency: Urgency::Medium,
source: "memory-health".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
});
}
match get_cached_memory_stats(agent_id) {
Some(stats) => {
// Alert if storage is very large (> 50MB)
if stats.storage_size_bytes > 50 * 1024 * 1024 {
return Some(HeartbeatAlert {
title: "记忆存储过大".to_string(),
content: format!(
"记忆存储已达 {:.1}MB建议清理低重要性记忆或归档旧记忆",
stats.storage_size_bytes as f64 / (1024.0 * 1024.0)
),
urgency: Urgency::Medium,
source: "memory-health".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
});
}
// Alert if too many memories (> 1000)
if stats.total_entries > 1000 {
return Some(HeartbeatAlert {
title: "记忆条目过多".to_string(),
content: format!(
"当前有 {} 条记忆,可能影响检索效率,建议清理或归档",
stats.total_entries
),
urgency: Urgency::Low,
source: "memory-health".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
});
// Alert if too many memories (> 1000)
if stats.total_entries > 1000 {
return Some(HeartbeatAlert {
title: "记忆条目过多".to_string(),
content: format!(
"当前有 {} 条记忆,可能影响检索效率,建议清理或归档",
stats.total_entries
),
urgency: Urgency::Low,
source: "memory-health".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
});
}
None
},
None => {
// Cache is empty - skip check (already reported in check_pending_tasks)
None
}
}
None
}
/// Check if user has been idle (placeholder)
fn check_idle_greeting(_agent_id: &str) -> Option<HeartbeatAlert> {
// In full implementation, this would check last interaction time
None
/// Check if user has been idle and might benefit from a greeting
fn check_idle_greeting(agent_id: &str) -> Option<HeartbeatAlert> {
let map = get_last_interaction_map();
// Try to get the last interaction time
let last_interaction = {
let read_result = map.read();
match read_result {
Ok(map) => map.get(agent_id).cloned(),
Err(_) => return None, // Skip if lock fails
}
};
// If no interaction recorded yet, skip
let last_interaction = last_interaction?;
// Parse the timestamp and convert to UTC for comparison
let last_time = chrono::DateTime::parse_from_rfc3339(&last_interaction)
.ok()?
.with_timezone(&chrono::Utc);
let now = chrono::Utc::now();
let idle_hours = (now - last_time).num_hours();
// Alert if idle for more than 24 hours
if idle_hours >= 24 {
Some(HeartbeatAlert {
title: "用户长时间未互动".to_string(),
content: format!(
"距离上次互动已过去 {} 小时,可以考虑主动问候或检查用户是否需要帮助",
idle_hours
),
urgency: Urgency::Low,
source: "idle-greeting".to_string(),
timestamp: now.to_rfc3339(),
})
} else {
None
}
}
/// Check for personality improvement opportunities
@@ -665,6 +732,16 @@ pub async fn heartbeat_record_correction(
Ok(())
}
/// Record a user interaction for idle greeting detection
/// Call this from frontend whenever user sends a message
#[tauri::command]
pub async fn heartbeat_record_interaction(
agent_id: String,
) -> Result<(), String> {
record_interaction(&agent_id);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -10,12 +10,12 @@
//! Phase 3 of Intelligence Layer Migration.
//! Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.3
use chrono::{DateTime, Utc};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tracing::{error, info, warn};
use tracing::{error, warn};
// === Types ===

View File

@@ -29,24 +29,10 @@ pub mod reflection;
pub mod identity;
// Re-export main types for convenience
pub use heartbeat::{
HeartbeatConfig, HeartbeatEngine, HeartbeatEngineState,
HeartbeatAlert, HeartbeatResult, HeartbeatStatus,
Urgency, NotifyChannel, ProactivityLevel,
};
pub use compactor::{
CompactionConfig, ContextCompactor, CompactableMessage,
CompactionResult, CompactionCheck, CompactionUrgency,
estimate_tokens, estimate_messages_tokens,
};
pub use heartbeat::HeartbeatEngineState;
pub use reflection::{
ReflectionConfig, ReflectionEngine, ReflectionEngineState,
ReflectionResult, ReflectionState, ReflectionResult as ReflectionOutput,
PatternObservation, ImprovementSuggestion, IdentityChangeProposal as ReflectionIdentityChangeProposal,
Sentiment, Priority, MemoryEntryForAnalysis,
ReflectionEngine, ReflectionEngineState,
};
pub use identity::{
AgentIdentityManager, IdentityManagerState,
IdentityFiles, IdentityChangeProposal, IdentitySnapshot,
IdentityFile, ProposalStatus,
};

View File

@@ -174,6 +174,13 @@ pub async fn kernel_init(
zclaw_kernel::config::KernelConfig::default()
};
// Debug: print skills directory
if let Some(ref skills_dir) = config.skills_dir {
println!("[kernel_init] Skills directory: {} (exists: {})", skills_dir.display(), skills_dir.exists());
} else {
println!("[kernel_init] No skills directory configured");
}
let base_url = config.llm.base_url.clone();
let model = config.llm.model.clone();
@@ -353,6 +360,8 @@ pub enum StreamChatEvent {
ToolStart { name: String, input: serde_json::Value },
/// Tool use completed
ToolEnd { name: String, output: serde_json::Value },
/// New iteration started (multi-turn tool calling)
IterationStart { iteration: usize, max_iterations: usize },
/// Stream completed
Complete { input_tokens: u32, output_tokens: u32 },
/// Error occurred
@@ -406,24 +415,38 @@ pub async fn agent_chat_stream(
tokio::spawn(async move {
use zclaw_runtime::LoopEvent;
println!("[agent_chat_stream] Starting to process stream events for session: {}", session_id);
while let Some(event) = rx.recv().await {
println!("[agent_chat_stream] Received event: {:?}", event);
let stream_event = match event {
LoopEvent::Delta(delta) => {
println!("[agent_chat_stream] Delta: {} bytes", delta.len());
StreamChatEvent::Delta { delta }
}
LoopEvent::ToolStart { name, input } => {
println!("[agent_chat_stream] ToolStart: {} input={:?}", name, input);
StreamChatEvent::ToolStart { name, input }
}
LoopEvent::ToolEnd { name, output } => {
println!("[agent_chat_stream] ToolEnd: {} output={:?}", name, output);
StreamChatEvent::ToolEnd { name, output }
}
LoopEvent::IterationStart { iteration, max_iterations } => {
println!("[agent_chat_stream] IterationStart: {}/{}", iteration, max_iterations);
StreamChatEvent::IterationStart { iteration, max_iterations }
}
LoopEvent::Complete(result) => {
println!("[agent_chat_stream] Complete: input_tokens={}, output_tokens={}",
result.input_tokens, result.output_tokens);
StreamChatEvent::Complete {
input_tokens: result.input_tokens,
output_tokens: result.output_tokens,
}
}
LoopEvent::Error(message) => {
println!("[agent_chat_stream] Error: {}", message);
StreamChatEvent::Error { message }
}
};
@@ -434,6 +457,8 @@ pub async fn agent_chat_stream(
"event": stream_event
}));
}
println!("[agent_chat_stream] Stream ended for session: {}", session_id);
});
Ok(())
@@ -460,6 +485,8 @@ pub struct SkillInfoResponse {
pub tags: Vec<String>,
pub mode: String,
pub enabled: bool,
pub triggers: Vec<String>,
pub category: Option<String>,
}
impl From<zclaw_skills::SkillManifest> for SkillInfoResponse {
@@ -473,6 +500,8 @@ impl From<zclaw_skills::SkillManifest> for SkillInfoResponse {
tags: manifest.tags,
mode: format!("{:?}", manifest.mode),
enabled: manifest.enabled,
triggers: manifest.triggers,
category: manifest.category,
}
}
}
@@ -491,6 +520,10 @@ pub async fn skill_list(
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
let skills = kernel.list_skills().await;
println!("[skill_list] Found {} skills", skills.len());
for skill in &skills {
println!("[skill_list] - {} ({})", skill.name, skill.id);
}
Ok(skills.into_iter().map(SkillInfoResponse::from).collect())
}
@@ -603,22 +636,67 @@ pub struct HandInfoResponse {
pub id: String,
pub name: String,
pub description: String,
pub status: String,
pub requirements_met: bool,
pub needs_approval: bool,
pub dependencies: Vec<String>,
pub tags: Vec<String>,
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(default)]
pub tool_count: u32,
#[serde(default)]
pub metric_count: u32,
}
impl From<zclaw_hands::HandConfig> for HandInfoResponse {
fn from(config: zclaw_hands::HandConfig) -> Self {
// Determine status based on enabled and dependencies
let status = if !config.enabled {
"unavailable".to_string()
} else if config.needs_approval {
"needs_approval".to_string()
} else {
"idle".to_string()
};
// Extract category from tags if present
let category = config.tags.iter().find(|t| {
["research", "automation", "browser", "data", "media", "communication"].contains(&t.as_str())
}).cloned();
// Map tags to icon
let icon = if config.tags.contains(&"browser".to_string()) {
Some("globe".to_string())
} else if config.tags.contains(&"research".to_string()) {
Some("search".to_string())
} else if config.tags.contains(&"media".to_string()) {
Some("video".to_string())
} else if config.tags.contains(&"data".to_string()) {
Some("database".to_string())
} else if config.tags.contains(&"communication".to_string()) {
Some("message-circle".to_string())
} else {
Some("zap".to_string())
};
Self {
id: config.id,
name: config.name,
description: config.description,
status,
requirements_met: config.enabled && config.dependencies.is_empty(),
needs_approval: config.needs_approval,
dependencies: config.dependencies,
tags: config.tags,
enabled: config.enabled,
category,
icon,
tool_count: 0,
metric_count: 0,
}
}
}

View File

@@ -13,13 +13,7 @@ pub mod persistent;
pub mod crypto;
// Re-export main types for convenience
pub use extractor::{SessionExtractor, ExtractedMemory, ExtractionConfig};
pub use context_builder::{ContextBuilder, EnhancedContext, ContextLevel};
pub use persistent::{
PersistentMemory, PersistentMemoryStore, MemorySearchQuery, MemoryStats,
generate_memory_id,
};
pub use crypto::{
CryptoError, KEY_SIZE, MEMORY_ENCRYPTION_KEY_NAME,
derive_key, generate_key, encrypt, decrypt,
};

View File

@@ -15,7 +15,7 @@ use tokio::sync::Mutex;
use uuid::Uuid;
use tauri::Manager;
use sqlx::{SqliteConnection, Connection, Row, sqlite::SqliteRow};
use chrono::{DateTime, Utc};
use chrono::Utc;
/// Memory entry stored in SQLite
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -6,7 +6,7 @@
use crate::memory::{PersistentMemory, PersistentMemoryStore, MemorySearchQuery, MemoryStats, generate_memory_id};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::{AppHandle, Manager, State};
use tauri::{AppHandle, State};
use tokio::sync::Mutex;
use chrono::Utc;