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
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(loop_guard): 为LoopGuard添加Clone派生 feat(capabilities): 实现CapabilityManager.validate()安全验证 fix(agentStore): 添加token用量追踪 chore: 删除未实现的Predictor/Lead HAND.toml文件 style(Credits): 移除假数据并标注开发中状态 refactor(Skills): 动态加载技能卡片 perf(configStore): 为定时任务添加localStorage降级 docs: 更新功能文档和版本变更记录
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<dyn LlmDriver>,
|
||||
tools: ToolRegistry,
|
||||
memory: Arc<MemoryStore>,
|
||||
#[allow(dead_code)] // Reserved for future rate limiting
|
||||
loop_guard: LoopGuard,
|
||||
loop_guard: Mutex<LoopGuard>,
|
||||
model: String,
|
||||
system_prompt: Option<String>,
|
||||
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,
|
||||
|
||||
@@ -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 (
|
||||
<div className="max-w-3xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
@@ -24,9 +17,10 @@ export function Credits() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-center mb-8 py-12">
|
||||
<div className="text-xs text-gray-500 mb-1">总积分</div>
|
||||
<div className="text-3xl font-bold text-gray-900">2268</div>
|
||||
<div className="text-3xl font-bold text-gray-900">--</div>
|
||||
<div className="text-xs text-gray-400 mt-2">积分系统开发中</div>
|
||||
</div>
|
||||
|
||||
<div className="p-1 mb-6 flex rounded-lg bg-gray-50 border border-gray-100 shadow-sm">
|
||||
@@ -50,18 +44,9 @@ export function Credits() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm">
|
||||
{logs.map((log) => (
|
||||
<div key={log.id} className="flex justify-between items-center p-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-700">{log.action}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{log.date}</div>
|
||||
</div>
|
||||
<div className={`font-medium ${log.amount < 0 ? 'text-gray-500' : 'text-green-500'}`}>
|
||||
{log.amount > 0 ? '+' : ''}{log.amount}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
||||
<div className="text-sm text-gray-400">暂无积分记录</div>
|
||||
<div className="text-xs text-gray-300 mt-1">连接后端服务后即可查看积分使用记录</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 系统技能 */}
|
||||
{/* 系统技能 — 从 skillsCatalog 动态加载,未连接时展示说明 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3">ZCLAW 系统技能</h3>
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3">
|
||||
ZCLAW 系统技能
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-blue-50 text-blue-600 rounded ml-2">
|
||||
{connected ? `已加载 ${skillsCatalog.filter(s => s.source === 'builtin').length} 个` : '未连接'}
|
||||
</span>
|
||||
</h3>
|
||||
{connected && skillsCatalog.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{SYSTEM_SKILLS.map((skill) => {
|
||||
const Icon = skill.icon;
|
||||
return (
|
||||
{skillsCatalog.slice(0, 16).map((skill) => (
|
||||
<div
|
||||
key={skill.id}
|
||||
className="bg-white rounded-xl border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Icon className="w-5 h-5 text-white" />
|
||||
<Wrench className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">{skill.name}</span>
|
||||
{skill.source && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-purple-50 text-purple-600 rounded">
|
||||
{skill.category}
|
||||
{skill.source === 'builtin' ? '内置' : '额外'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">{skill.description}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{skill.description || skill.path || skill.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
{skillsCatalog.length > 16 && (
|
||||
<div className="text-xs text-gray-400 text-center col-span-2 py-2">
|
||||
还有 {skillsCatalog.length - 16} 个技能未展示
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center shadow-sm">
|
||||
<p className="text-sm text-gray-400">
|
||||
{connected ? '暂无可用技能' : '连接后端后自动加载系统技能列表'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 mb-6 shadow-sm">
|
||||
|
||||
@@ -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<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
||||
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<void> => {
|
||||
if (isTauriEnv()) {
|
||||
await intelligence.memory.init();
|
||||
if (isTauriRuntime()) {
|
||||
await tauriInvoke('memory.init', () => intelligence.memory.init());
|
||||
} else {
|
||||
await fallbackMemory.init();
|
||||
}
|
||||
},
|
||||
|
||||
store: async (entry: MemoryEntryInput): Promise<string> => {
|
||||
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<MemoryEntry | null> => {
|
||||
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<MemoryEntry[]> => {
|
||||
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<void> => {
|
||||
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<number> => {
|
||||
if (isTauriEnv()) {
|
||||
return intelligence.memory.deleteAll(agentId);
|
||||
if (isTauriRuntime()) {
|
||||
return tauriInvoke('memory.deleteAll', () => intelligence.memory.deleteAll(agentId));
|
||||
}
|
||||
return fallbackMemory.deleteAll(agentId);
|
||||
},
|
||||
|
||||
stats: async (): Promise<MemoryStats> => {
|
||||
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<MemoryEntry[]> => {
|
||||
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<number> => {
|
||||
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<string> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<HeartbeatResult> => {
|
||||
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<HeartbeatConfig> => {
|
||||
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<void> => {
|
||||
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<HeartbeatResult[]> => {
|
||||
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,15 +1182,17 @@ export const intelligenceClient = {
|
||||
totalEntries: number,
|
||||
storageSizeBytes: number
|
||||
): Promise<void> => {
|
||||
if (isTauriEnv()) {
|
||||
await invoke('heartbeat_update_memory_stats', {
|
||||
if (isTauriRuntime()) {
|
||||
await tauriInvoke('heartbeat.updateMemoryStats', () =>
|
||||
invoke('heartbeat_update_memory_stats', {
|
||||
agent_id: agentId,
|
||||
task_count: taskCount,
|
||||
total_entries: totalEntries,
|
||||
storage_size_bytes: storageSizeBytes,
|
||||
});
|
||||
}
|
||||
// Fallback: store in localStorage for non-Tauri environment
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Browser/dev fallback only
|
||||
const cache = {
|
||||
taskCount,
|
||||
totalEntries,
|
||||
@@ -1172,45 +1200,56 @@ export const intelligenceClient = {
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem(`zclaw-memory-stats-${agentId}`, JSON.stringify(cache));
|
||||
}
|
||||
},
|
||||
|
||||
recordCorrection: async (agentId: string, correctionType: string): Promise<void> => {
|
||||
if (isTauriEnv()) {
|
||||
await invoke('heartbeat_record_correction', {
|
||||
if (isTauriRuntime()) {
|
||||
await tauriInvoke('heartbeat.recordCorrection', () =>
|
||||
invoke('heartbeat_record_correction', {
|
||||
agent_id: agentId,
|
||||
correction_type: correctionType,
|
||||
});
|
||||
}
|
||||
// Fallback: store in localStorage for non-Tauri environment
|
||||
})
|
||||
);
|
||||
} 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));
|
||||
}
|
||||
},
|
||||
|
||||
recordInteraction: async (agentId: string): Promise<void> => {
|
||||
if (isTauriEnv()) {
|
||||
await invoke('heartbeat_record_interaction', {
|
||||
if (isTauriRuntime()) {
|
||||
await tauriInvoke('heartbeat.recordInteraction', () =>
|
||||
invoke('heartbeat_record_interaction', {
|
||||
agent_id: agentId,
|
||||
});
|
||||
}
|
||||
// Fallback: store in localStorage for non-Tauri environment
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Browser/dev fallback only
|
||||
localStorage.setItem(`zclaw-last-interaction-${agentId}`, new Date().toISOString());
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
compactor: {
|
||||
estimateTokens: async (text: string): Promise<number> => {
|
||||
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<number> => {
|
||||
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<CompactionCheck> => {
|
||||
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<CompactionResult> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
if (isTauriEnv()) {
|
||||
await intelligence.reflection.recordConversation();
|
||||
if (isTauriRuntime()) {
|
||||
await tauriInvoke('reflection.recordConversation', () =>
|
||||
intelligence.reflection.recordConversation()
|
||||
);
|
||||
} else {
|
||||
await fallbackReflection.recordConversation();
|
||||
}
|
||||
},
|
||||
|
||||
shouldReflect: async (): Promise<boolean> => {
|
||||
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<ReflectionResult> => {
|
||||
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<ReflectionResult[]> => {
|
||||
if (isTauriEnv()) {
|
||||
return intelligence.reflection.getHistory(limit);
|
||||
if (isTauriRuntime()) {
|
||||
return tauriInvoke('reflection.getHistory', () =>
|
||||
intelligence.reflection.getHistory(limit)
|
||||
);
|
||||
}
|
||||
return fallbackReflection.getHistory(limit);
|
||||
},
|
||||
|
||||
getState: async (): Promise<ReflectionState> => {
|
||||
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<IdentityFiles> => {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<IdentityChangeProposal> => {
|
||||
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<IdentityFiles> => {
|
||||
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<void> => {
|
||||
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<IdentityChangeProposal[]> => {
|
||||
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<void> => {
|
||||
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<IdentitySnapshot[]> => {
|
||||
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<void> => {
|
||||
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<string[]> => {
|
||||
if (isTauriEnv()) {
|
||||
return intelligence.identity.listAgents();
|
||||
if (isTauriRuntime()) {
|
||||
return tauriInvoke('identity.listAgents', () => intelligence.identity.listAgents());
|
||||
}
|
||||
return fallbackIdentity.listAgents();
|
||||
},
|
||||
|
||||
deleteAgent: async (agentId: string): Promise<void> => {
|
||||
if (isTauriEnv()) {
|
||||
await intelligence.identity.deleteAgent(agentId);
|
||||
if (isTauriRuntime()) {
|
||||
await tauriInvoke('identity.deleteAgent', () => intelligence.identity.deleteAgent(agentId));
|
||||
} else {
|
||||
await fallbackIdentity.deleteAgent(agentId);
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ export const useAgentStore = create<AgentStore>((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<AgentStore>((set, get) => ({
|
||||
const stats: UsageStats = {
|
||||
totalSessions: conversations.length,
|
||||
totalMessages,
|
||||
totalTokens: 0,
|
||||
totalTokens: tokenData.total,
|
||||
byModel: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Message>) => 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<ChatState>()(
|
||||
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<ChatState>()(
|
||||
};
|
||||
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<ChatState>()(
|
||||
),
|
||||
});
|
||||
|
||||
// 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<ChatState>()(
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
@@ -395,9 +395,18 @@ export const useConfigStore = create<ConfigStateSlice & ConfigActionsSlice>((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<ConfigStateSlice & ConfigActionsSlice>((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();
|
||||
|
||||
@@ -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<string, unknown> for HandClient compatibility
|
||||
const hands: Array<Record<string, unknown>> = 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<string, unknown> || 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<string, unknown>).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);
|
||||
}
|
||||
|
||||
@@ -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<PipelineInfo[]>('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<PipelineInfo>('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<string, unknown>) => {
|
||||
try {
|
||||
const result = await invoke<RunPipelineResponse>('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<PipelineRunResponse[]>('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);
|
||||
}
|
||||
|
||||
@@ -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%) 因核心反思逻辑尚未深度迭代。
|
||||
|
||||
@@ -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 # 销售数据保留更久
|
||||
@@ -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 = "生成包含可视化的分析报告"
|
||||
Reference in New Issue
Block a user