From eed347e1a6d8eb558e9466e0944f9d97c7b12325 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 27 Mar 2026 07:56:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E9=98=B2=E6=8A=A4=E5=92=8C=E5=AE=89=E5=85=A8=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(loop_guard): 为LoopGuard添加Clone派生 feat(capabilities): 实现CapabilityManager.validate()安全验证 fix(agentStore): 添加token用量追踪 chore: 删除未实现的Predictor/Lead HAND.toml文件 style(Credits): 移除假数据并标注开发中状态 refactor(Skills): 动态加载技能卡片 perf(configStore): 为定时任务添加localStorage降级 docs: 更新功能文档和版本变更记录 --- crates/zclaw-kernel/src/capabilities.rs | 30 +- crates/zclaw-runtime/src/loop_guard.rs | 2 +- crates/zclaw-runtime/src/loop_runner.rs | 63 +++- desktop/src/components/Settings/Credits.tsx | 27 +- desktop/src/components/Settings/Skills.tsx | 107 ++----- desktop/src/lib/intelligence-client.ts | 329 ++++++++++++-------- desktop/src/store/agentStore.ts | 3 +- desktop/src/store/chatStore.ts | 27 +- desktop/src/store/configStore.ts | 40 ++- desktop/src/store/handStore.ts | 169 +++++++++- desktop/src/store/workflowStore.ts | 161 +++++++++- docs/features/README.md | 16 +- hands/lead.HAND.toml | 77 ----- hands/predictor.HAND.toml | 149 --------- 14 files changed, 724 insertions(+), 476 deletions(-) delete mode 100644 hands/lead.HAND.toml delete mode 100644 hands/predictor.HAND.toml diff --git a/crates/zclaw-kernel/src/capabilities.rs b/crates/zclaw-kernel/src/capabilities.rs index b3aa2bf..54edf75 100644 --- a/crates/zclaw-kernel/src/capabilities.rs +++ b/crates/zclaw-kernel/src/capabilities.rs @@ -1,7 +1,7 @@ //! Capability manager use dashmap::DashMap; -use zclaw_types::{AgentId, Capability, CapabilitySet, Result}; +use zclaw_types::{AgentId, Capability, CapabilitySet, Result, ZclawError}; /// Manages capabilities for all agents pub struct CapabilityManager { @@ -52,9 +52,31 @@ impl CapabilityManager { .unwrap_or(false) } - /// Validate capabilities don't exceed parent's - pub fn validate(&self, _capabilities: &[Capability]) -> Result<()> { - // TODO: Implement capability validation + /// Validate capabilities for dangerous combinations + /// + /// Checks that overly broad capabilities are not combined with + /// dangerous operations. Returns an error if an unsafe combination + /// is detected. + pub fn validate(&self, capabilities: &[Capability]) -> Result<()> { + let has_tool_all = capabilities.iter().any(|c| matches!(c, Capability::ToolAll)); + let has_agent_kill = capabilities.iter().any(|c| matches!(c, Capability::AgentKill { .. })); + let has_shell_wildcard = capabilities.iter().any(|c| { + matches!(c, Capability::ShellExec { pattern } if pattern == "*") + }); + + // ToolAll + destructive operations is dangerous + if has_tool_all && has_agent_kill { + return Err(ZclawError::SecurityError( + "ToolAll 与 AgentKill 不能同时授予".to_string(), + )); + } + + if has_tool_all && has_shell_wildcard { + return Err(ZclawError::SecurityError( + "ToolAll 与 ShellExec(\"*\") 不能同时授予".to_string(), + )); + } + Ok(()) } diff --git a/crates/zclaw-runtime/src/loop_guard.rs b/crates/zclaw-runtime/src/loop_guard.rs index dcc315c..571c16a 100644 --- a/crates/zclaw-runtime/src/loop_guard.rs +++ b/crates/zclaw-runtime/src/loop_guard.rs @@ -25,7 +25,7 @@ impl Default for LoopGuardConfig { } /// Loop guard state -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct LoopGuard { config: LoopGuardConfig, /// Hash of (tool_name, params) -> count diff --git a/crates/zclaw-runtime/src/loop_runner.rs b/crates/zclaw-runtime/src/loop_runner.rs index 6a1d2b8..4ba8797 100644 --- a/crates/zclaw-runtime/src/loop_runner.rs +++ b/crates/zclaw-runtime/src/loop_runner.rs @@ -1,6 +1,7 @@ //! Agent loop implementation use std::sync::Arc; +use std::sync::Mutex; use futures::StreamExt; use tokio::sync::mpsc; use zclaw_types::{AgentId, SessionId, Message, Result}; @@ -9,7 +10,7 @@ use crate::driver::{LlmDriver, CompletionRequest, ContentBlock}; use crate::stream::StreamChunk; use crate::tool::{ToolRegistry, ToolContext, SkillExecutor}; use crate::tool::builtin::PathValidator; -use crate::loop_guard::LoopGuard; +use crate::loop_guard::{LoopGuard, LoopGuardResult}; use crate::growth::GrowthIntegration; use crate::compaction; use zclaw_memory::MemoryStore; @@ -20,8 +21,7 @@ pub struct AgentLoop { driver: Arc, tools: ToolRegistry, memory: Arc, - #[allow(dead_code)] // Reserved for future rate limiting - loop_guard: LoopGuard, + loop_guard: Mutex, model: String, system_prompt: Option, max_tokens: u32, @@ -46,7 +46,7 @@ impl AgentLoop { driver, tools, memory, - loop_guard: LoopGuard::default(), + loop_guard: Mutex::new(LoopGuard::default()), model: String::new(), // Must be set via with_model() system_prompt: None, max_tokens: 4096, @@ -235,7 +235,28 @@ impl AgentLoop { // Create tool context and execute all tools let tool_context = self.create_tool_context(session_id.clone()); + let mut circuit_breaker_triggered = false; for (id, name, input) in tool_calls { + // Check loop guard before executing tool + let guard_result = self.loop_guard.lock().unwrap().check(&name, &input); + match guard_result { + LoopGuardResult::CircuitBreaker => { + tracing::warn!("[AgentLoop] Circuit breaker triggered by tool '{}'", name); + circuit_breaker_triggered = true; + break; + } + LoopGuardResult::Blocked => { + tracing::warn!("[AgentLoop] Tool '{}' blocked by loop guard", name); + let error_output = serde_json::json!({ "error": "工具调用被循环防护拦截" }); + messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true)); + continue; + } + LoopGuardResult::Warn => { + tracing::warn!("[AgentLoop] Tool '{}' triggered loop guard warning", name); + } + LoopGuardResult::Allowed => {} + } + let tool_result = match self.execute_tool(&name, input, &tool_context).await { Ok(result) => result, Err(e) => serde_json::json!({ "error": e.to_string() }), @@ -251,6 +272,18 @@ impl AgentLoop { } // Continue the loop - LLM will process tool results and generate final response + + // If circuit breaker was triggered, terminate immediately + if circuit_breaker_triggered { + let msg = "检测到工具调用循环,已自动终止"; + self.memory.append_message(&session_id, &Message::assistant(msg)).await?; + break AgentLoopResult { + response: msg.to_string(), + input_tokens: total_input_tokens, + output_tokens: total_output_tokens, + iterations, + }; + } }; // Process conversation for memory extraction (post-conversation) @@ -299,6 +332,7 @@ impl AgentLoop { let memory = self.memory.clone(); let driver = self.driver.clone(); let tools = self.tools.clone(); + let loop_guard_clone = self.loop_guard.lock().unwrap().clone(); let skill_executor = self.skill_executor.clone(); let path_validator = self.path_validator.clone(); let agent_id = self.agent_id.clone(); @@ -308,6 +342,7 @@ impl AgentLoop { tokio::spawn(async move { let mut messages = messages; + let loop_guard_clone = Mutex::new(loop_guard_clone); let max_iterations = 10; let mut iteration = 0; let mut total_input_tokens = 0u32; @@ -423,6 +458,26 @@ impl AgentLoop { // Execute tools for (id, name, input) in pending_tool_calls { tracing::debug!("[AgentLoop] Executing tool: name={}, input={:?}", name, input); + + // Check loop guard before executing tool + let guard_result = loop_guard_clone.lock().unwrap().check(&name, &input); + match guard_result { + LoopGuardResult::CircuitBreaker => { + let _ = tx.send(LoopEvent::Error("检测到工具调用循环,已自动终止".to_string())).await; + break 'outer; + } + LoopGuardResult::Blocked => { + tracing::warn!("[AgentLoop] Tool '{}' blocked by loop guard", name); + let error_output = serde_json::json!({ "error": "工具调用被循环防护拦截" }); + let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true)); + continue; + } + LoopGuardResult::Warn => { + tracing::warn!("[AgentLoop] Tool '{}' triggered loop guard warning", name); + } + LoopGuardResult::Allowed => {} + } let tool_context = ToolContext { agent_id: agent_id.clone(), working_directory: None, diff --git a/desktop/src/components/Settings/Credits.tsx b/desktop/src/components/Settings/Credits.tsx index ff912cd..57ac6e4 100644 --- a/desktop/src/components/Settings/Credits.tsx +++ b/desktop/src/components/Settings/Credits.tsx @@ -3,13 +3,6 @@ import { useState } from 'react'; export function Credits() { const [filter, setFilter] = useState<'all' | 'consume' | 'earn'>('all'); - const logs = [ - { id: 1, action: 'AutoClaw网页搜索', date: '2026年03月11日 12:02:02', amount: -6 }, - { id: 2, action: 'AutoClaw网页搜索', date: '2026年03月11日 12:01:58', amount: -6 }, - { id: 3, action: 'AutoClaw网页搜索', date: '2026年03月11日 12:01:46', amount: -6 }, - { id: 4, action: 'AutoClaw网页搜索', date: '2026年03月11日 12:01:43', amount: -6 }, - ]; - return (
@@ -24,9 +17,10 @@ export function Credits() {
-
+
总积分
-
2268
+
--
+
积分系统开发中
@@ -50,18 +44,9 @@ export function Credits() {
-
- {logs.map((log) => ( -
-
-
{log.action}
-
{log.date}
-
-
- {log.amount > 0 ? '+' : ''}{log.amount} -
-
- ))} +
+
暂无积分记录
+
连接后端服务后即可查看积分使用记录
); diff --git a/desktop/src/components/Settings/Skills.tsx b/desktop/src/components/Settings/Skills.tsx index 1be3aca..831b28a 100644 --- a/desktop/src/components/Settings/Skills.tsx +++ b/desktop/src/components/Settings/Skills.tsx @@ -2,67 +2,7 @@ import { useEffect, useState } from 'react'; import { useConnectionStore } from '../../store/connectionStore'; import { useConfigStore } from '../../store/configStore'; import { silentErrorHandler } from '../../lib/error-utils'; -import { Wrench, Zap, FileCode, Globe, Mail, Database, Search, MessageSquare } from 'lucide-react'; - -// ZCLAW 内置系统技能 -const SYSTEM_SKILLS = [ - { - id: 'code-assistant', - name: '代码助手', - description: '代码编写、调试、重构和优化', - category: '开发', - icon: FileCode, - }, - { - id: 'web-search', - name: '网络搜索', - description: '实时搜索互联网信息', - category: '信息', - icon: Search, - }, - { - id: 'file-manager', - name: '文件管理', - description: '文件读写、搜索和整理', - category: '系统', - icon: Database, - }, - { - id: 'web-browsing', - name: '网页浏览', - description: '访问和解析网页内容', - category: '信息', - icon: Globe, - }, - { - id: 'email-handler', - name: '邮件处理', - description: '发送和管理电子邮件', - category: '通讯', - icon: Mail, - }, - { - id: 'chat-skill', - name: '对话技能', - description: '自然语言对话和问答', - category: '交互', - icon: MessageSquare, - }, - { - id: 'automation', - name: '自动化任务', - description: '自动化工作流程执行', - category: '系统', - icon: Zap, - }, - { - id: 'tool-executor', - name: '工具执行器', - description: '执行系统命令和脚本', - category: '系统', - icon: Wrench, - }, -]; +import { Wrench } from 'lucide-react'; export function Skills() { const connectionState = useConnectionStore((s) => s.connectionState); @@ -116,35 +56,52 @@ export function Skills() {
)} - {/* 系统技能 */} + {/* 系统技能 — 从 skillsCatalog 动态加载,未连接时展示说明 */}
-

ZCLAW 系统技能

-
- {SYSTEM_SKILLS.map((skill) => { - const Icon = skill.icon; - return ( +

+ ZCLAW 系统技能 + + {connected ? `已加载 ${skillsCatalog.filter(s => s.source === 'builtin').length} 个` : '未连接'} + +

+ {connected && skillsCatalog.length > 0 ? ( +
+ {skillsCatalog.slice(0, 16).map((skill) => (
- +
{skill.name} - - {skill.category} - + {skill.source && ( + + {skill.source === 'builtin' ? '内置' : '额外'} + + )}
-

{skill.description}

+

{skill.description || skill.path || skill.id}

- ); - })} -
+ ))} + {skillsCatalog.length > 16 && ( +
+ 还有 {skillsCatalog.length - 16} 个技能未展示 +
+ )} +
+ ) : ( +
+

+ {connected ? '暂无可用技能' : '连接后端后自动加载系统技能列表'} +

+
+ )}
diff --git a/desktop/src/lib/intelligence-client.ts b/desktop/src/lib/intelligence-client.ts index efde8d6..2534d9d 100644 --- a/desktop/src/lib/intelligence-client.ts +++ b/desktop/src/lib/intelligence-client.ts @@ -3,7 +3,16 @@ * * Provides a unified API for intelligence operations that: * - Uses Rust backend (via Tauri commands) when running in Tauri environment - * - Falls back to localStorage-based implementation in browser environment + * - Falls back to localStorage-based implementation in browser/dev environment + * + * Degradation strategy: + * - In Tauri mode: if a Tauri invoke fails, the error is logged and re-thrown. + * The caller is responsible for handling the error. We do NOT silently fall + * back to localStorage, because that would give users degraded functionality + * (localStorage instead of SQLite, rule-based instead of LLM-based, no-op + * instead of real execution) without any indication that something is wrong. + * - In browser/dev mode: localStorage fallback is the intended behavior for + * development and testing without a Tauri backend. * * This replaces direct usage of: * - agent-memory.ts @@ -38,6 +47,8 @@ import { invoke } from '@tauri-apps/api/core'; +import { isTauriRuntime } from './tauri-gateway'; + import { intelligence, type MemoryEntryInput, @@ -62,15 +73,6 @@ import { type IdentitySnapshot, } from './intelligence-backend'; -// === Environment Detection === - -/** - * Check if running in Tauri environment - */ -export function isTauriEnv(): boolean { - return typeof window !== 'undefined' && '__TAURI__' in window; -} - // === Frontend Types (for backward compatibility) === export type MemoryType = 'fact' | 'preference' | 'lesson' | 'context' | 'task'; @@ -982,75 +984,91 @@ const fallbackHeartbeat = { // === Unified Client Export === /** - * Unified intelligence client that automatically selects backend or fallback + * Helper: wrap a Tauri invoke call so that failures are logged and re-thrown + * instead of silently falling back to localStorage implementations. + */ +function tauriInvoke(label: string, fn: () => Promise): Promise { + return fn().catch((e: unknown) => { + console.warn(`[IntelligenceClient] Tauri invoke failed (${label}):`, e); + throw e; + }); +} + +/** + * Unified intelligence client that automatically selects backend or fallback. + * + * - In Tauri mode: calls Rust backend via invoke(). On failure, logs a warning + * and re-throws -- does NOT fall back to localStorage. + * - In browser/dev mode: uses localStorage-based fallback implementations. */ export const intelligenceClient = { memory: { init: async (): Promise => { - if (isTauriEnv()) { - await intelligence.memory.init(); + if (isTauriRuntime()) { + await tauriInvoke('memory.init', () => intelligence.memory.init()); } else { await fallbackMemory.init(); } }, store: async (entry: MemoryEntryInput): Promise => { - if (isTauriEnv()) { - return intelligence.memory.store(entry); + if (isTauriRuntime()) { + return tauriInvoke('memory.store', () => intelligence.memory.store(entry)); } return fallbackMemory.store(entry); }, get: async (id: string): Promise => { - if (isTauriEnv()) { - const result = await intelligence.memory.get(id); + if (isTauriRuntime()) { + const result = await tauriInvoke('memory.get', () => intelligence.memory.get(id)); return result ? toFrontendMemory(result) : null; } return fallbackMemory.get(id); }, search: async (options: MemorySearchOptions): Promise => { - if (isTauriEnv()) { - const results = await intelligence.memory.search(toBackendSearchOptions(options)); + if (isTauriRuntime()) { + const results = await tauriInvoke('memory.search', () => + intelligence.memory.search(toBackendSearchOptions(options)) + ); return results.map(toFrontendMemory); } return fallbackMemory.search(options); }, delete: async (id: string): Promise => { - if (isTauriEnv()) { - await intelligence.memory.delete(id); + if (isTauriRuntime()) { + await tauriInvoke('memory.delete', () => intelligence.memory.delete(id)); } else { await fallbackMemory.delete(id); } }, deleteAll: async (agentId: string): Promise => { - if (isTauriEnv()) { - return intelligence.memory.deleteAll(agentId); + if (isTauriRuntime()) { + return tauriInvoke('memory.deleteAll', () => intelligence.memory.deleteAll(agentId)); } return fallbackMemory.deleteAll(agentId); }, stats: async (): Promise => { - if (isTauriEnv()) { - const stats = await intelligence.memory.stats(); + if (isTauriRuntime()) { + const stats = await tauriInvoke('memory.stats', () => intelligence.memory.stats()); return toFrontendStats(stats); } return fallbackMemory.stats(); }, export: async (): Promise => { - if (isTauriEnv()) { - const results = await intelligence.memory.export(); + if (isTauriRuntime()) { + const results = await tauriInvoke('memory.export', () => intelligence.memory.export()); return results.map(toFrontendMemory); } return fallbackMemory.export(); }, import: async (memories: MemoryEntry[]): Promise => { - if (isTauriEnv()) { - // Convert to backend format + if (isTauriRuntime()) { const backendMemories = memories.map(m => ({ ...m, agent_id: m.agentId, @@ -1062,14 +1080,16 @@ export const intelligenceClient = { tags: JSON.stringify(m.tags), embedding: null, })); - return intelligence.memory.import(backendMemories as PersistentMemory[]); + return tauriInvoke('memory.import', () => + intelligence.memory.import(backendMemories as PersistentMemory[]) + ); } return fallbackMemory.import(memories); }, dbPath: async (): Promise => { - if (isTauriEnv()) { - return intelligence.memory.dbPath(); + if (isTauriRuntime()) { + return tauriInvoke('memory.dbPath', () => intelligence.memory.dbPath()); } return fallbackMemory.dbPath(); }, @@ -1079,10 +1099,12 @@ export const intelligenceClient = { query: string, maxTokens?: number, ): Promise<{ systemPromptAddition: string; totalTokens: number; memoriesUsed: number }> => { - if (isTauriEnv()) { - return intelligence.memory.buildContext(agentId, query, maxTokens ?? null); + if (isTauriRuntime()) { + return tauriInvoke('memory.buildContext', () => + intelligence.memory.buildContext(agentId, query, maxTokens ?? null) + ); } - // Fallback: use basic search + // Browser/dev fallback: use basic search const memories = await fallbackMemory.search({ agentId, query, @@ -1098,54 +1120,58 @@ export const intelligenceClient = { heartbeat: { init: async (agentId: string, config?: HeartbeatConfig): Promise => { - if (isTauriEnv()) { - await intelligence.heartbeat.init(agentId, config); + if (isTauriRuntime()) { + await tauriInvoke('heartbeat.init', () => intelligence.heartbeat.init(agentId, config)); } else { await fallbackHeartbeat.init(agentId, config); } }, start: async (agentId: string): Promise => { - if (isTauriEnv()) { - await intelligence.heartbeat.start(agentId); + if (isTauriRuntime()) { + await tauriInvoke('heartbeat.start', () => intelligence.heartbeat.start(agentId)); } else { await fallbackHeartbeat.start(agentId); } }, stop: async (agentId: string): Promise => { - if (isTauriEnv()) { - await intelligence.heartbeat.stop(agentId); + if (isTauriRuntime()) { + await tauriInvoke('heartbeat.stop', () => intelligence.heartbeat.stop(agentId)); } else { await fallbackHeartbeat.stop(agentId); } }, tick: async (agentId: string): Promise => { - if (isTauriEnv()) { - return intelligence.heartbeat.tick(agentId); + if (isTauriRuntime()) { + return tauriInvoke('heartbeat.tick', () => intelligence.heartbeat.tick(agentId)); } return fallbackHeartbeat.tick(agentId); }, getConfig: async (agentId: string): Promise => { - if (isTauriEnv()) { - return intelligence.heartbeat.getConfig(agentId); + if (isTauriRuntime()) { + return tauriInvoke('heartbeat.getConfig', () => intelligence.heartbeat.getConfig(agentId)); } return fallbackHeartbeat.getConfig(agentId); }, updateConfig: async (agentId: string, config: HeartbeatConfig): Promise => { - if (isTauriEnv()) { - await intelligence.heartbeat.updateConfig(agentId, config); + if (isTauriRuntime()) { + await tauriInvoke('heartbeat.updateConfig', () => + intelligence.heartbeat.updateConfig(agentId, config) + ); } else { await fallbackHeartbeat.updateConfig(agentId, config); } }, getHistory: async (agentId: string, limit?: number): Promise => { - if (isTauriEnv()) { - return intelligence.heartbeat.getHistory(agentId, limit); + if (isTauriRuntime()) { + return tauriInvoke('heartbeat.getHistory', () => + intelligence.heartbeat.getHistory(agentId, limit) + ); } return fallbackHeartbeat.getHistory(agentId, limit); }, @@ -1156,61 +1182,74 @@ export const intelligenceClient = { totalEntries: number, storageSizeBytes: number ): Promise => { - if (isTauriEnv()) { - await invoke('heartbeat_update_memory_stats', { - agent_id: agentId, - task_count: taskCount, - total_entries: totalEntries, - storage_size_bytes: storageSizeBytes, - }); + if (isTauriRuntime()) { + await tauriInvoke('heartbeat.updateMemoryStats', () => + invoke('heartbeat_update_memory_stats', { + agent_id: agentId, + task_count: taskCount, + total_entries: totalEntries, + storage_size_bytes: storageSizeBytes, + }) + ); + } else { + // Browser/dev fallback only + const cache = { + taskCount, + totalEntries, + storageSizeBytes, + lastUpdated: new Date().toISOString(), + }; + localStorage.setItem(`zclaw-memory-stats-${agentId}`, JSON.stringify(cache)); } - // Fallback: store in localStorage for non-Tauri environment - const cache = { - taskCount, - totalEntries, - storageSizeBytes, - lastUpdated: new Date().toISOString(), - }; - localStorage.setItem(`zclaw-memory-stats-${agentId}`, JSON.stringify(cache)); }, recordCorrection: async (agentId: string, correctionType: string): Promise => { - if (isTauriEnv()) { - await invoke('heartbeat_record_correction', { - agent_id: agentId, - correction_type: correctionType, - }); + if (isTauriRuntime()) { + await tauriInvoke('heartbeat.recordCorrection', () => + invoke('heartbeat_record_correction', { + agent_id: agentId, + correction_type: correctionType, + }) + ); + } else { + // Browser/dev fallback only + const key = `zclaw-corrections-${agentId}`; + const stored = localStorage.getItem(key); + const counters = stored ? JSON.parse(stored) : {}; + counters[correctionType] = (counters[correctionType] || 0) + 1; + localStorage.setItem(key, JSON.stringify(counters)); } - // Fallback: store in localStorage for non-Tauri environment - const key = `zclaw-corrections-${agentId}`; - const stored = localStorage.getItem(key); - const counters = stored ? JSON.parse(stored) : {}; - counters[correctionType] = (counters[correctionType] || 0) + 1; - localStorage.setItem(key, JSON.stringify(counters)); }, recordInteraction: async (agentId: string): Promise => { - if (isTauriEnv()) { - await invoke('heartbeat_record_interaction', { - agent_id: agentId, - }); + if (isTauriRuntime()) { + await tauriInvoke('heartbeat.recordInteraction', () => + invoke('heartbeat_record_interaction', { + agent_id: agentId, + }) + ); + } else { + // Browser/dev fallback only + localStorage.setItem(`zclaw-last-interaction-${agentId}`, new Date().toISOString()); } - // Fallback: store in localStorage for non-Tauri environment - localStorage.setItem(`zclaw-last-interaction-${agentId}`, new Date().toISOString()); }, }, compactor: { estimateTokens: async (text: string): Promise => { - if (isTauriEnv()) { - return intelligence.compactor.estimateTokens(text); + if (isTauriRuntime()) { + return tauriInvoke('compactor.estimateTokens', () => + intelligence.compactor.estimateTokens(text) + ); } return fallbackCompactor.estimateTokens(text); }, estimateMessagesTokens: async (messages: CompactableMessage[]): Promise => { - if (isTauriEnv()) { - return intelligence.compactor.estimateMessagesTokens(messages); + if (isTauriRuntime()) { + return tauriInvoke('compactor.estimateMessagesTokens', () => + intelligence.compactor.estimateMessagesTokens(messages) + ); } return fallbackCompactor.estimateMessagesTokens(messages); }, @@ -1219,8 +1258,10 @@ export const intelligenceClient = { messages: CompactableMessage[], config?: CompactionConfig ): Promise => { - if (isTauriEnv()) { - return intelligence.compactor.checkThreshold(messages, config); + if (isTauriRuntime()) { + return tauriInvoke('compactor.checkThreshold', () => + intelligence.compactor.checkThreshold(messages, config) + ); } return fallbackCompactor.checkThreshold(messages, config); }, @@ -1231,8 +1272,10 @@ export const intelligenceClient = { conversationId?: string, config?: CompactionConfig ): Promise => { - if (isTauriEnv()) { - return intelligence.compactor.compact(messages, agentId, conversationId, config); + if (isTauriRuntime()) { + return tauriInvoke('compactor.compact', () => + intelligence.compactor.compact(messages, agentId, conversationId, config) + ); } return fallbackCompactor.compact(messages, agentId, conversationId, config); }, @@ -1240,45 +1283,53 @@ export const intelligenceClient = { reflection: { init: async (config?: ReflectionConfig): Promise => { - if (isTauriEnv()) { - await intelligence.reflection.init(config); + if (isTauriRuntime()) { + await tauriInvoke('reflection.init', () => intelligence.reflection.init(config)); } else { await fallbackReflection.init(config); } }, recordConversation: async (): Promise => { - if (isTauriEnv()) { - await intelligence.reflection.recordConversation(); + if (isTauriRuntime()) { + await tauriInvoke('reflection.recordConversation', () => + intelligence.reflection.recordConversation() + ); } else { await fallbackReflection.recordConversation(); } }, shouldReflect: async (): Promise => { - if (isTauriEnv()) { - return intelligence.reflection.shouldReflect(); + if (isTauriRuntime()) { + return tauriInvoke('reflection.shouldReflect', () => + intelligence.reflection.shouldReflect() + ); } return fallbackReflection.shouldReflect(); }, reflect: async (agentId: string, memories: MemoryEntryForAnalysis[]): Promise => { - if (isTauriEnv()) { - return intelligence.reflection.reflect(agentId, memories); + if (isTauriRuntime()) { + return tauriInvoke('reflection.reflect', () => + intelligence.reflection.reflect(agentId, memories) + ); } return fallbackReflection.reflect(agentId, memories); }, getHistory: async (limit?: number): Promise => { - if (isTauriEnv()) { - return intelligence.reflection.getHistory(limit); + if (isTauriRuntime()) { + return tauriInvoke('reflection.getHistory', () => + intelligence.reflection.getHistory(limit) + ); } return fallbackReflection.getHistory(limit); }, getState: async (): Promise => { - if (isTauriEnv()) { - return intelligence.reflection.getState(); + if (isTauriRuntime()) { + return tauriInvoke('reflection.getState', () => intelligence.reflection.getState()); } return fallbackReflection.getState(); }, @@ -1286,37 +1337,43 @@ export const intelligenceClient = { identity: { get: async (agentId: string): Promise => { - if (isTauriEnv()) { - return intelligence.identity.get(agentId); + if (isTauriRuntime()) { + return tauriInvoke('identity.get', () => intelligence.identity.get(agentId)); } return fallbackIdentity.get(agentId); }, getFile: async (agentId: string, file: string): Promise => { - if (isTauriEnv()) { - return intelligence.identity.getFile(agentId, file); + if (isTauriRuntime()) { + return tauriInvoke('identity.getFile', () => intelligence.identity.getFile(agentId, file)); } return fallbackIdentity.getFile(agentId, file); }, buildPrompt: async (agentId: string, memoryContext?: string): Promise => { - if (isTauriEnv()) { - return intelligence.identity.buildPrompt(agentId, memoryContext); + if (isTauriRuntime()) { + return tauriInvoke('identity.buildPrompt', () => + intelligence.identity.buildPrompt(agentId, memoryContext) + ); } return fallbackIdentity.buildPrompt(agentId, memoryContext); }, updateUserProfile: async (agentId: string, content: string): Promise => { - if (isTauriEnv()) { - await intelligence.identity.updateUserProfile(agentId, content); + if (isTauriRuntime()) { + await tauriInvoke('identity.updateUserProfile', () => + intelligence.identity.updateUserProfile(agentId, content) + ); } else { await fallbackIdentity.updateUserProfile(agentId, content); } }, appendUserProfile: async (agentId: string, addition: string): Promise => { - if (isTauriEnv()) { - await intelligence.identity.appendUserProfile(agentId, addition); + if (isTauriRuntime()) { + await tauriInvoke('identity.appendUserProfile', () => + intelligence.identity.appendUserProfile(agentId, addition) + ); } else { await fallbackIdentity.appendUserProfile(agentId, addition); } @@ -1328,67 +1385,81 @@ export const intelligenceClient = { suggestedContent: string, reason: string ): Promise => { - if (isTauriEnv()) { - return intelligence.identity.proposeChange(agentId, file, suggestedContent, reason); + if (isTauriRuntime()) { + return tauriInvoke('identity.proposeChange', () => + intelligence.identity.proposeChange(agentId, file, suggestedContent, reason) + ); } return fallbackIdentity.proposeChange(agentId, file, suggestedContent, reason); }, approveProposal: async (proposalId: string): Promise => { - if (isTauriEnv()) { - return intelligence.identity.approveProposal(proposalId); + if (isTauriRuntime()) { + return tauriInvoke('identity.approveProposal', () => + intelligence.identity.approveProposal(proposalId) + ); } return fallbackIdentity.approveProposal(proposalId); }, rejectProposal: async (proposalId: string): Promise => { - if (isTauriEnv()) { - await intelligence.identity.rejectProposal(proposalId); + if (isTauriRuntime()) { + await tauriInvoke('identity.rejectProposal', () => + intelligence.identity.rejectProposal(proposalId) + ); } else { await fallbackIdentity.rejectProposal(proposalId); } }, getPendingProposals: async (agentId?: string): Promise => { - if (isTauriEnv()) { - return intelligence.identity.getPendingProposals(agentId); + if (isTauriRuntime()) { + return tauriInvoke('identity.getPendingProposals', () => + intelligence.identity.getPendingProposals(agentId) + ); } return fallbackIdentity.getPendingProposals(agentId); }, updateFile: async (agentId: string, file: string, content: string): Promise => { - if (isTauriEnv()) { - await intelligence.identity.updateFile(agentId, file, content); + if (isTauriRuntime()) { + await tauriInvoke('identity.updateFile', () => + intelligence.identity.updateFile(agentId, file, content) + ); } else { await fallbackIdentity.updateFile(agentId, file, content); } }, getSnapshots: async (agentId: string, limit?: number): Promise => { - if (isTauriEnv()) { - return intelligence.identity.getSnapshots(agentId, limit); + if (isTauriRuntime()) { + return tauriInvoke('identity.getSnapshots', () => + intelligence.identity.getSnapshots(agentId, limit) + ); } return fallbackIdentity.getSnapshots(agentId, limit); }, restoreSnapshot: async (agentId: string, snapshotId: string): Promise => { - if (isTauriEnv()) { - await intelligence.identity.restoreSnapshot(agentId, snapshotId); + if (isTauriRuntime()) { + await tauriInvoke('identity.restoreSnapshot', () => + intelligence.identity.restoreSnapshot(agentId, snapshotId) + ); } else { await fallbackIdentity.restoreSnapshot(agentId, snapshotId); } }, listAgents: async (): Promise => { - if (isTauriEnv()) { - return intelligence.identity.listAgents(); + if (isTauriRuntime()) { + return tauriInvoke('identity.listAgents', () => intelligence.identity.listAgents()); } return fallbackIdentity.listAgents(); }, deleteAgent: async (agentId: string): Promise => { - if (isTauriEnv()) { - await intelligence.identity.deleteAgent(agentId); + if (isTauriRuntime()) { + await tauriInvoke('identity.deleteAgent', () => intelligence.identity.deleteAgent(agentId)); } else { await fallbackIdentity.deleteAgent(agentId); } diff --git a/desktop/src/store/agentStore.ts b/desktop/src/store/agentStore.ts index a6ab1dd..3de1112 100644 --- a/desktop/src/store/agentStore.ts +++ b/desktop/src/store/agentStore.ts @@ -212,6 +212,7 @@ export const useAgentStore = create((set, get) => ({ loadUsageStats: async () => { try { const { conversations } = useChatStore.getState(); + const tokenData = useChatStore.getState().getTotalTokens(); let totalMessages = 0; for (const conversation of conversations) { @@ -225,7 +226,7 @@ export const useAgentStore = create((set, get) => ({ const stats: UsageStats = { totalSessions: conversations.length, totalMessages, - totalTokens: 0, + totalTokens: tokenData.total, byModel: {}, }; diff --git a/desktop/src/store/chatStore.ts b/desktop/src/store/chatStore.ts index 9e0742f..79ef106 100644 --- a/desktop/src/store/chatStore.ts +++ b/desktop/src/store/chatStore.ts @@ -85,6 +85,9 @@ interface ChatState { isLoading: boolean; currentModel: string; sessionKey: string | null; + // Token usage tracking + totalInputTokens: number; + totalOutputTokens: number; addMessage: (message: Message) => void; updateMessage: (id: string, updates: Partial) => void; @@ -97,6 +100,8 @@ interface ChatState { newConversation: () => void; switchConversation: (id: string) => void; deleteConversation: (id: string) => void; + addTokenUsage: (inputTokens: number, outputTokens: number) => void; + getTotalTokens: () => { input: number; output: number; total: number }; searchSkills: (query: string) => { results: Array<{ id: string; name: string; description: string }>; totalAvailable: number }; } @@ -194,8 +199,10 @@ export const useChatStore = create()( isLoading: false, currentModel: 'glm-4-flash', sessionKey: null, + totalInputTokens: 0, + totalOutputTokens: 0, - addMessage: (message) => + addMessage: (message: Message) => set((state) => ({ messages: [...state.messages, message] })), updateMessage: (id, updates) => @@ -432,7 +439,7 @@ export const useChatStore = create()( }; set((state) => ({ messages: [...state.messages, handMsg] })); }, - onComplete: () => { + onComplete: (inputTokens?: number, outputTokens?: number) => { const state = get(); // Save conversation to persist across refresh @@ -448,6 +455,11 @@ export const useChatStore = create()( ), }); + // Track token usage if provided (KernelClient provides these) + if (inputTokens !== undefined && outputTokens !== undefined) { + get().addTokenUsage(inputTokens, outputTokens); + } + // Async memory extraction after stream completes const msgs = get().messages .filter(m => m.role === 'user' || m.role === 'assistant') @@ -518,6 +530,17 @@ export const useChatStore = create()( } }, + addTokenUsage: (inputTokens: number, outputTokens: number) => + set((state) => ({ + totalInputTokens: state.totalInputTokens + inputTokens, + totalOutputTokens: state.totalOutputTokens + outputTokens, + })), + + getTotalTokens: () => { + const { totalInputTokens, totalOutputTokens } = get(); + return { input: totalInputTokens, output: totalOutputTokens, total: totalInputTokens + totalOutputTokens }; + }, + searchSkills: (query: string) => { const discovery = getSkillDiscovery(); const result = discovery.searchSkills(query); diff --git a/desktop/src/store/configStore.ts b/desktop/src/store/configStore.ts index 4183067..7b0178a 100644 --- a/desktop/src/store/configStore.ts +++ b/desktop/src/store/configStore.ts @@ -395,9 +395,18 @@ export const useConfigStore = create((set try { const result = await client.listScheduledTasks(); - set({ scheduledTasks: result?.tasks || [] }); + const tasks = result?.tasks || []; + set({ scheduledTasks: tasks }); + // Persist to localStorage as fallback + try { localStorage.setItem('zclaw-scheduled-tasks', JSON.stringify(tasks)); } catch { /* ignore */ } } catch { - // Ignore if heartbeat.tasks not available + // Fallback: load from localStorage + try { + const stored = localStorage.getItem('zclaw-scheduled-tasks'); + if (stored) { + set({ scheduledTasks: JSON.parse(stored) }); + } + } catch { /* ignore */ } } }, @@ -416,9 +425,11 @@ export const useConfigStore = create((set nextRun: result.nextRun, description: result.description, }; - set((state) => ({ - scheduledTasks: [...state.scheduledTasks, newTask], - })); + set((state) => { + const tasks = [...state.scheduledTasks, newTask]; + try { localStorage.setItem('zclaw-scheduled-tasks', JSON.stringify(tasks)); } catch { /* ignore */ } + return { scheduledTasks: tasks }; + }); return newTask; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Failed to create scheduled task'; @@ -602,8 +613,23 @@ function createConfigClientFromKernel(client: KernelClient): ConfigStoreClient { return null; } }, - getQuickConfig: async () => ({ quickConfig: {} }), - saveQuickConfig: async () => null, + getQuickConfig: async () => { + // Read from localStorage in kernel mode + try { + const stored = localStorage.getItem('zclaw-quick-config'); + if (stored) { + return { quickConfig: JSON.parse(stored) }; + } + } catch { /* ignore */ } + return { quickConfig: {} }; + }, + saveQuickConfig: async (config) => { + // Persist to localStorage in kernel mode + try { + localStorage.setItem('zclaw-quick-config', JSON.stringify(config)); + } catch { /* ignore */ } + return { quickConfig: config }; + }, listSkills: async () => { try { const result = await client.listSkills(); diff --git a/desktop/src/store/handStore.ts b/desktop/src/store/handStore.ts index e9660fd..6f7b40a 100644 --- a/desktop/src/store/handStore.ts +++ b/desktop/src/store/handStore.ts @@ -522,13 +522,180 @@ export function createHandClientFromGateway(client: GatewayClient): HandClient { }; } +// === Kernel Client Adapter === + +import type { KernelClient } from '../lib/kernel-client'; + +/** + * Helper to create a HandClient adapter from a KernelClient. + * Maps KernelClient methods (Tauri invoke) to the HandClient interface. + */ +function createHandClientFromKernel(client: KernelClient): HandClient { + return { + listHands: async () => { + try { + const result = await client.listHands(); + // KernelClient returns typed objects; cast to Record for HandClient compatibility + const hands: Array> = result.hands.map((h) => ({ + id: h.id || h.name, + name: h.name, + description: h.description, + status: h.status, + requirements_met: h.requirements_met, + category: h.category, + icon: h.icon, + tool_count: h.tool_count, + tools: h.tools, + metric_count: h.metric_count, + metrics: h.metrics, + })); + return { hands }; + } catch { + return null; + } + }, + getHand: async (name: string) => { + try { + const result = await client.getHand(name); + return result as Record || null; + } catch { + return null; + } + }, + listHandRuns: async (name: string, opts) => { + try { + const result = await client.listHandRuns(name, opts); + return result as unknown as { runs?: RawHandRun[] } | null; + } catch { + return null; + } + }, + triggerHand: async (name: string, params) => { + try { + const result = await client.triggerHand(name, params); + return { runId: result.runId, status: result.status }; + } catch { + return null; + } + }, + approveHand: async (name: string, runId: string, approved: boolean, reason?: string) => { + return client.approveHand(name, runId, approved, reason); + }, + cancelHand: async (name: string, runId: string) => { + return client.cancelHand(name, runId); + }, + listTriggers: async () => { + try { + const result = await client.listTriggers(); + if (!result?.triggers) return { triggers: [] }; + // Map KernelClient trigger shape to HandClient Trigger shape + const triggers: Trigger[] = result.triggers.map((t) => ({ + id: t.id, + type: t.triggerType, + enabled: t.enabled, + })); + return { triggers }; + } catch { + return { triggers: [] }; + } + }, + getTrigger: async (id: string) => { + try { + const result = await client.getTrigger(id); + if (!result) return null; + return { + id: result.id, + type: result.triggerType, + enabled: result.enabled, + } as Trigger; + } catch { + return null; + } + }, + createTrigger: async (trigger) => { + try { + const result = await client.createTrigger({ + id: `${trigger.type}_${Date.now()}`, + name: trigger.name || trigger.type, + handId: trigger.handName || '', + triggerType: { type: trigger.type }, + enabled: trigger.enabled, + description: trigger.config ? JSON.stringify(trigger.config) : undefined, + }); + return result ? { id: result.id } : null; + } catch { + return null; + } + }, + updateTrigger: async (id: string, updates) => { + const result = await client.updateTrigger(id, { + name: updates.name, + enabled: updates.enabled, + handId: updates.handName, + triggerType: updates.config ? { type: (updates.config as Record).type as string } : undefined, + }); + return { id: result.id }; + }, + deleteTrigger: async (id: string) => { + await client.deleteTrigger(id); + return { status: 'deleted' }; + }, + listApprovals: async () => { + try { + const result = await client.listApprovals(); + // Map KernelClient approval shape to HandClient RawApproval shape + const approvals: RawApproval[] = (result?.approvals || []).map((a) => ({ + id: a.id, + hand_id: a.handId, + status: a.status, + requestedAt: a.createdAt, + })); + return { approvals }; + } catch { + return { approvals: [] }; + } + }, + respondToApproval: async (approvalId: string, approved: boolean, reason?: string) => { + await client.respondToApproval(approvalId, approved, reason); + return { status: approved ? 'approved' : 'rejected' }; + }, + }; +} + // === Client Injection === /** * Sets the client for the hand store. * Called by the coordinator during initialization. + * Detects whether the client is a KernelClient (Tauri) or GatewayClient (browser). */ export function setHandStoreClient(client: unknown): void { - const handClient = createHandClientFromGateway(client as GatewayClient); + let handClient: HandClient; + + // Check if it's a KernelClient (has listHands method that returns typed objects) + if (client && typeof client === 'object' && 'listHands' in client) { + handClient = createHandClientFromKernel(client as KernelClient); + } else if (client && typeof client === 'object') { + // It's a GatewayClient + handClient = createHandClientFromGateway(client as GatewayClient); + } else { + // Fallback: return a stub client that gracefully handles all calls + handClient = { + listHands: async () => null, + getHand: async () => null, + listHandRuns: async () => null, + triggerHand: async () => null, + approveHand: async () => ({ status: 'error' }), + cancelHand: async () => ({ status: 'error' }), + listTriggers: async () => ({ triggers: [] }), + getTrigger: async () => null, + createTrigger: async () => null, + updateTrigger: async () => ({ id: '' }), + deleteTrigger: async () => ({ status: 'error' }), + listApprovals: async () => ({ approvals: [] }), + respondToApproval: async () => ({ status: 'error' }), + }; + } + useHandStore.getState().setHandStoreClient(handClient); } diff --git a/desktop/src/store/workflowStore.ts b/desktop/src/store/workflowStore.ts index f2bf6ec..fde595b 100644 --- a/desktop/src/store/workflowStore.ts +++ b/desktop/src/store/workflowStore.ts @@ -1,5 +1,7 @@ import { create } from 'zustand'; +import { invoke } from '@tauri-apps/api/core'; import type { GatewayClient } from '../lib/gateway-client'; +import type { KernelClient } from '../lib/kernel-client'; // === Core Types (previously imported from gatewayStore) === @@ -327,11 +329,168 @@ function createWorkflowClientFromGateway(client: GatewayClient): WorkflowClient }; } +// === Pipeline types (from Tauri backend) === + +interface PipelineInfo { + id: string; + displayName: string; + description: string; + category: string; + industry: string; + tags: string[]; + icon: string; + version: string; + author: string; + inputs: Array<{ + name: string; + inputType: string; + required: boolean; + label: string; + placeholder?: string; + default?: unknown; + options: string[]; + }>; +} + +interface RunPipelineResponse { + runId: string; + pipelineId: string; + status: string; +} + +interface PipelineRunResponse { + runId: string; + pipelineId: string; + status: string; + currentStep?: string; + percentage: number; + message: string; + outputs?: unknown; + error?: string; + startedAt: string; + endedAt?: string; +} + +/** + * Helper to create a WorkflowClient adapter from a KernelClient. + * Uses direct Tauri invoke() calls to pipeline_commands since KernelClient + * does not have workflow methods (workflows in Tauri mode are pipelines). + */ +function createWorkflowClientFromKernel(_client: KernelClient): WorkflowClient { + return { + listWorkflows: async () => { + try { + const pipelines = await invoke('pipeline_list', {}); + if (!pipelines) return null; + return { + workflows: pipelines.map((p) => ({ + id: p.id, + name: p.displayName || p.id, + steps: p.inputs.length, + description: p.description, + createdAt: undefined, + })), + }; + } catch { + return null; + } + }, + getWorkflow: async (id: string) => { + try { + const pipeline = await invoke('pipeline_get', { pipelineId: id }); + return { + id: pipeline.id, + name: pipeline.displayName || pipeline.id, + description: pipeline.description, + steps: pipeline.inputs.map((input) => ({ + handName: input.inputType, + name: input.label, + params: input.default ? { default: input.default } : undefined, + })), + createdAt: undefined, + } satisfies WorkflowDetail; + } catch { + return null; + } + }, + createWorkflow: async () => { + throw new Error('Workflow creation not supported in KernelClient mode. Pipelines are file-based YAML definitions.'); + }, + updateWorkflow: async () => { + throw new Error('Workflow update not supported in KernelClient mode. Pipelines are file-based YAML definitions.'); + }, + deleteWorkflow: async () => { + throw new Error('Workflow deletion not supported in KernelClient mode. Pipelines are file-based YAML definitions.'); + }, + executeWorkflow: async (id: string, input?: Record) => { + try { + const result = await invoke('pipeline_run', { + request: { pipelineId: id, inputs: input || {} }, + }); + return { runId: result.runId, status: result.status }; + } catch { + return null; + } + }, + cancelWorkflow: async (_workflowId: string, runId: string) => { + try { + await invoke('pipeline_cancel', { runId }); + return { status: 'cancelled' }; + } catch { + return { status: 'error' }; + } + }, + listWorkflowRuns: async (workflowId: string) => { + try { + const runs = await invoke('pipeline_runs', {}); + // Filter runs by pipeline ID and map to RawWorkflowRun shape + const filteredRuns: RawWorkflowRun[] = runs + .filter((r) => r.pipelineId === workflowId) + .map((r) => ({ + run_id: r.runId, + workflow_id: r.pipelineId, + status: r.status, + started_at: r.startedAt, + completed_at: r.endedAt, + current_step: r.currentStep ? Math.round(r.percentage) : undefined, + error: r.error, + result: r.outputs, + })); + return { runs: filteredRuns }; + } catch { + return { runs: [] }; + } + }, + }; +} + /** * Sets the client for the workflow store. * Called by the coordinator during initialization. + * Detects whether the client is a KernelClient (Tauri) or GatewayClient (browser). */ export function setWorkflowStoreClient(client: unknown): void { - const workflowClient = createWorkflowClientFromGateway(client as GatewayClient); + let workflowClient: WorkflowClient; + + // Check if it's a KernelClient (has listHands method, which KernelClient has but GatewayClient doesn't) + if (client && typeof client === 'object' && 'listHands' in client) { + workflowClient = createWorkflowClientFromKernel(client as KernelClient); + } else if (client && typeof client === 'object') { + // It's a GatewayClient + workflowClient = createWorkflowClientFromGateway(client as GatewayClient); + } else { + // Fallback: return a stub client that gracefully handles all calls + workflowClient = { + listWorkflows: async () => null, + getWorkflow: async () => null, + createWorkflow: async () => null, + updateWorkflow: async () => null, + deleteWorkflow: async () => ({ status: 'error' }), + executeWorkflow: async () => null, + cancelWorkflow: async () => ({ status: 'error' }), + listWorkflowRuns: async () => null, + }; + } + useWorkflowStore.getState().setWorkflowStoreClient(workflowClient); } diff --git a/docs/features/README.md b/docs/features/README.md index 49f3133..47d8118 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -70,7 +70,7 @@ > ✅ **更新 (2026-03-26)**: > - 7 个 Hands 有完整 Rust 后端实现 (Browser, Collector, Researcher, Slideshow, Speech, Whiteboard, Quiz) > - ✅ **审批流程**: Kernel 新增 `pending_approvals` 管理,`hand_approve`/`hand_cancel` Tauri 命令已实现真实审批逻辑 -> - ⚠️ **Predictor** 和 **Lead** 仅有 HAND.toml 配置文件,无 Rust 实现 +> - ⚠️ **Predictor** 和 **Lead** 已删除(无 Rust 实现,配置文件已清理) > - ⚠️ **Clip** 需要 FFmpeg,**Twitter** 需要 API Key ### 1.7 Tauri 后端 @@ -232,9 +232,9 @@ | **Rust Crates** | **9** (types, memory, runtime, kernel, skills, hands, protocols, pipeline, growth) | | **SKILL.md 文件** | **78+** | | 动态发现技能 | 78+ (100%) | -| Hands 总数 | 11 | -| **已实现 Hands** | **7 (64%)** | -| **Kernel 注册 Hands** | **7/9 (78%)** | +| Hands 总数 | 10 | +| **已实现 Hands** | **7 (70%)** | +| **Kernel 注册 Hands** | **7/7 (100%)** | | **Pipeline 模板** | **5** (教育/营销/法律/研究/生产力) | | Zustand Store | **18+** | | Tauri 命令 | **80+** | @@ -285,6 +285,7 @@ skills hands protocols pipeline growth channels | 日期 | 版本 | 变更内容 | |------|------|---------| +| 2026-03-27 | v0.6.1 | **功能完整性修复**: 激活 LoopGuard 循环防护、实现 CapabilityManager.validate() 安全验证、handStore/workflowStore KernelClient 适配器、Credits 标注开发中、Skills 动态化、ScheduledTasks localStorage 降级、token 用量追踪 | | 2026-03-27 | v0.6.0a | **全面审计更新**:所有成熟度标注调整为实际完成度 (平均 68%),新增清理记录 | | 2026-03-26 | v0.1.0 | **v1.0 发布准备**:移除 Team/Swarm 功能(~8,100 行,Pipeline 替代),安全修复,CI/CD 建立 | | 2026-03-26 | v0.5.0 | **Smart Presentation Layer**:自动类型检测,Chart/Quiz/Slideshow/Document 渲染器,PresentationAnalyzer Rust 后端 | @@ -315,5 +316,12 @@ skills hands protocols pipeline growth channels | 标记 Wasm/Native SkillMode | 明确标注为尚未实现 | | 清理 browser/mod.rs | 移除未使用的 re-exports | | 清理 5 个死代码模块 | pattern_detector, recommender, mesh, persona_evolver, trigger_evaluator | +| 激活 LoopGuard | AgentLoop 循环防护已接入 (warn/block/circuit_breaker) | +| 实现 CapabilityManager.validate() | 安全验证:ToolAll+AgentKill、ToolAll+ShellExec(*) 组合拒绝 | +| 删除 Predictor/Lead HAND.toml | 无 Rust 实现的配置文件已彻底删除 | +| Credits.tsx 标注开发中 | 移除假数据,显示"开发中"占位 | +| Skills.tsx 动态化 | 移除硬编码系统技能卡片,改为从 skillsCatalog 动态加载 | +| ScheduledTasks 持久化 | 添加 localStorage 降级,刷新不丢失 | +| Token 用量追踪 | chatStore 新增 addTokenUsage/getTotalTokens | > **审计说明**: 成熟度等级已根据代码审计调整为实际值。Identity Evolution 标注为 L2 (70%) 是因为其 `dead_code` 属性属于 Tauri 运行时模式(在 Tauri 上下文中实际被调用),而非真正的死代码。Reflection Engine L2 (65%) 因核心反思逻辑尚未深度迭代。 diff --git a/hands/lead.HAND.toml b/hands/lead.HAND.toml deleted file mode 100644 index c2ea665..0000000 --- a/hands/lead.HAND.toml +++ /dev/null @@ -1,77 +0,0 @@ -# Lead Hand - 销售线索发现能力包 -# -# ZCLAW Hand 配置示例 -# 这个 Hand 自动发现和筛选销售线索 -# -# ⚠️ 注意: 此 Hand 尚未实现 Rust 后端,仅作为设计文档保留。 -# 启用状态设为 false,前端不会显示为可用能力。 - -[hand] -name = "lead" -version = "1.0.0" -description = "销售线索发现和筛选能力包 - 自动识别潜在客户(未实现)" -author = "ZCLAW Team" - -type = "automation" -enabled = false -requires_approval = true # 线索操作需要审批 -timeout = 600 -max_concurrent = 1 - -tags = ["sales", "leads", "automation", "discovery", "qualification"] - -[hand.config] -# 线索来源 -sources = ["linkedin", "company_website", "crunchbase", "public_records"] - -# 筛选条件 -[hand.config.filters] -# 最小公司规模 -min_company_size = 10 -# 目标行业 -industries = ["technology", "saas", "fintech", "healthcare"] -# 目标地区 -regions = ["china", "north_america", "europe"] - -# 评分权重 -[hand.config.scoring] -company_fit = 0.4 -engagement_likelihood = 0.3 -budget_indication = 0.2 -timing_signals = 0.1 - -[hand.triggers] -manual = true -schedule = true # 允许定时触发 -webhook = true - -# 定时触发:每天早上 9 点 -[[hand.triggers.schedules]] -cron = "0 9 * * 1-5" # 工作日 9:00 -enabled = true -timezone = "Asia/Shanghai" - -[hand.permissions] -requires = [ - "web.search", - "web.fetch", - "api.external", - "database.write" -] - -roles = ["operator.read", "operator.write", "sales.read"] - -[hand.approval] -# 审批流程配置 -timeout_hours = 24 -approvers = ["sales_manager", "admin"] -auto_approve_after_hours = 0 # 不自动批准 - -[hand.rate_limit] -max_requests = 100 -window_seconds = 86400 # 每天 - -[hand.audit] -log_inputs = true -log_outputs = true -retention_days = 90 # 销售数据保留更久 diff --git a/hands/predictor.HAND.toml b/hands/predictor.HAND.toml deleted file mode 100644 index 7ed2fd1..0000000 --- a/hands/predictor.HAND.toml +++ /dev/null @@ -1,149 +0,0 @@ -# Predictor Hand - 预测分析能力包 -# -# ZCLAW Hand 配置 -# 这个 Hand 提供预测分析、趋势预测和数据建模能力 -# -# ⚠️ 注意: 此 Hand 尚未实现 Rust 后端,仅作为设计文档保留。 -# 启用状态设为 false,前端不会显示为可用能力。 - -[hand] -name = "predictor" -version = "1.0.0" -description = "预测分析能力包 - 执行回归、分类和时间序列预测(未实现)" -author = "ZCLAW Team" - -# Hand 类型 -type = "data" - -# 未实现,禁用此 Hand -enabled = false - -# 是否需要人工审批才能执行 -requires_approval = false - -# 默认超时时间(秒) -timeout = 600 - -# 最大并发执行数 -max_concurrent = 2 - -# 能力标签 -tags = ["prediction", "analytics", "forecasting", "ml", "statistics"] - -[hand.config] -# 模型配置 -default_model = "auto" # auto, regression, classification, timeseries -model_storage = "/tmp/zclaw/predictor/models" - -# 训练配置 -train_test_split = 0.8 -cross_validation = 5 - -# 输出配置 -output_format = "report" # report, json, chart -include_visualization = true -confidence_level = 0.95 - -# 特征工程 -auto_feature_selection = true -max_features = 50 - -[hand.triggers] -# 触发器配置 -manual = true -schedule = true -webhook = false - -# 事件触发器 -[[hand.triggers.events]] -type = "data.updated" -pattern = ".*(forecast|predict|analyze).*" -priority = 7 - -[[hand.triggers.events]] -type = "chat.intent" -pattern = "预测|分析|趋势|forecast|predict|analyze|trend" -priority = 5 - -[hand.permissions] -# 权限要求 -requires = [ - "file.read", - "file.write", - "compute.ml" -] - -# RBAC 角色要求 -roles = ["operator.read", "operator.write"] - -# 速率限制 -[hand.rate_limit] -max_requests = 20 -window_seconds = 3600 # 1 hour - -# 审计配置 -[hand.audit] -log_inputs = true -log_outputs = true -retention_days = 30 - -# 参数定义 -[[hand.parameters]] -name = "dataSource" -label = "数据源" -type = "text" -required = true -description = "数据文件路径或 URL" - -[[hand.parameters]] -name = "model" -label = "模型类型" -type = "select" -required = true -options = ["regression", "classification", "timeseries"] -description = "预测模型的类型" - -[[hand.parameters]] -name = "targetColumn" -label = "目标列" -type = "text" -required = true -description = "要预测的目标变量列名" - -[[hand.parameters]] -name = "featureColumns" -label = "特征列" -type = "text" -required = false -description = "用于预测的特征列(逗号分隔,留空自动选择)" - -# 工作流步骤 -[[hand.workflow]] -id = "load" -name = "加载数据" -description = "读取和验证输入数据" - -[[hand.workflow]] -id = "preprocess" -name = "数据预处理" -description = "清洗数据、处理缺失值、特征工程" - -[[hand.workflow]] -id = "train" -name = "训练模型" -description = "训练预测模型并进行交叉验证" - -[[hand.workflow]] -id = "evaluate" -name = "评估模型" -description = "计算模型性能指标" - -[[hand.workflow]] -id = "predict" -name = "执行预测" -description = "使用训练好的模型进行预测" - -[[hand.workflow]] -id = "report" -name = "生成报告" -description = "生成包含可视化的分析报告"