diff --git a/desktop/src-tauri/src/kernel_commands/mod.rs b/desktop/src-tauri/src/kernel_commands/mod.rs index 686c5bf..88535ea 100644 --- a/desktop/src-tauri/src/kernel_commands/mod.rs +++ b/desktop/src-tauri/src/kernel_commands/mod.rs @@ -17,6 +17,7 @@ pub mod orchestration; pub mod scheduled_task; pub mod skill; pub mod trigger; +pub mod workspace; #[cfg(feature = "multi-agent")] pub mod a2a; diff --git a/desktop/src-tauri/src/kernel_commands/workspace.rs b/desktop/src-tauri/src/kernel_commands/workspace.rs new file mode 100644 index 0000000..e12f08f --- /dev/null +++ b/desktop/src-tauri/src/kernel_commands/workspace.rs @@ -0,0 +1,43 @@ +//! Workspace directory statistics command + +use serde::Serialize; +use std::path::Path; + +#[derive(Serialize)] +pub struct DirStats { + pub file_count: u64, + pub total_size: u64, +} + +/// Count files and total size in a directory (non-recursive, top-level only) +#[tauri::command] +pub async fn workspace_dir_stats(path: String) -> Result { + let dir = Path::new(&path); + if !dir.exists() { + return Ok(DirStats { + file_count: 0, + total_size: 0, + }); + } + if !dir.is_dir() { + return Err(format!("{} is not a directory", path)); + } + + let mut file_count: u64 = 0; + let mut total_size: u64 = 0; + + let entries = std::fs::read_dir(dir).map_err(|e| format!("Failed to read dir: {}", e))?; + for entry in entries.flatten() { + if let Ok(metadata) = entry.metadata() { + if metadata.is_file() { + file_count += 1; + total_size += metadata.len(); + } + } + } + + Ok(DirStats { + file_count, + total_size, + }) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index b2ac78a..f92fe40 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -323,6 +323,8 @@ pub fn run() { kernel_commands::trigger::trigger_update, kernel_commands::trigger::trigger_delete, kernel_commands::trigger::trigger_execute, + // Workspace commands + kernel_commands::workspace::workspace_dir_stats, // Approval management commands kernel_commands::approval::approval_list, kernel_commands::approval::approval_respond, diff --git a/desktop/src/components/Settings/MCPServices.tsx b/desktop/src/components/Settings/MCPServices.tsx index 63e28e4..9a01fc5 100644 --- a/desktop/src/components/Settings/MCPServices.tsx +++ b/desktop/src/components/Settings/MCPServices.tsx @@ -180,14 +180,19 @@ export function MCPServices() { ); }) : ( -
- 当前快速配置中尚未声明 MCP 服务 +
+
尚未配置 MCP 服务
+
+ MCP 服务为 Agent 扩展外部工具能力。可通过编辑配置文件 + config/mcp.toml + 添加服务。 +
)}
- 新增服务、删除服务和详细参数配置尚未在桌面端接入。可通过配置文件手动添加。 + 新增/删除服务尚未在桌面端 UI 接入。可通过编辑 config/mcp.toml 手动添加 MCP 服务配置,重启后生效。
); diff --git a/desktop/src/components/Settings/Skills.tsx b/desktop/src/components/Settings/Skills.tsx index 831b28a..5b7b7e4 100644 --- a/desktop/src/components/Settings/Skills.tsx +++ b/desktop/src/components/Settings/Skills.tsx @@ -17,6 +17,9 @@ export function Skills() { useEffect(() => { if (connected) { loadSkillsCatalog().catch(silentErrorHandler('Skills')); + } else { + // In Tauri mode, try loading even without 'connected' state + loadSkillsCatalog().catch(() => { /* direct invoke fallback handles this */ }); } }, [connected]); diff --git a/desktop/src/components/Settings/Workspace.tsx b/desktop/src/components/Settings/Workspace.tsx index 98fbf57..997677e 100644 --- a/desktop/src/components/Settings/Workspace.tsx +++ b/desktop/src/components/Settings/Workspace.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; import { useConfigStore } from '../../store/configStore'; import { silentErrorHandler } from '../../lib/error-utils'; @@ -8,6 +9,17 @@ export function Workspace() { const loadWorkspaceInfo = useConfigStore((s) => s.loadWorkspaceInfo); const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig); const [projectDir, setProjectDir] = useState('~/.zclaw/zclaw-workspace'); + const [dirStats, setDirStats] = useState<{ fileCount: number; totalSize: number } | null>(null); + + // Load real directory stats via Tauri command + const loadDirStats = async (dir: string) => { + try { + const stats = await invoke<{ file_count: number; total_size: number }>('workspace_dir_stats', { path: dir }); + setDirStats({ fileCount: stats.file_count, totalSize: stats.total_size }); + } catch { + // Command may not exist in all modes — fallback to workspaceInfo + } + }; useEffect(() => { loadWorkspaceInfo().catch(silentErrorHandler('Workspace')); @@ -22,6 +34,18 @@ export function Workspace() { setProjectDir(nextValue); await saveQuickConfig({ workspaceDir: nextValue }); await loadWorkspaceInfo(); + await loadDirStats(nextValue); + }; + + const handleBrowse = async () => { + // Dialog plugin not available — user types path manually + const dir = prompt('输入工作区目录路径:', projectDir); + if (dir && dir.trim()) { + setProjectDir(dir.trim()); + await saveQuickConfig({ workspaceDir: dir.trim() }); + await loadWorkspaceInfo(); + await loadDirStats(dir.trim()); + } }; const handleToggle = async ( @@ -51,13 +75,20 @@ export function Workspace() { onBlur={() => { handleWorkspaceBlur().catch(silentErrorHandler('Workspace')); }} className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm bg-gray-50 focus:outline-none" /> -
-
当前解析路径:{workspaceInfo?.resolvedPath || '未解析'}
-
文件数:{workspaceInfo?.fileCount ?? 0},大小:{workspaceInfo?.totalSize ?? 0} bytes
+
当前解析路径:{workspaceInfo?.resolvedPath || projectDir}
+
+ 文件数:{dirStats?.fileCount ?? workspaceInfo?.fileCount ?? 0} + {dirStats && `,大小:${(dirStats.totalSize / 1024).toFixed(1)} KB`} + {!dirStats && workspaceInfo?.totalSize ? `,大小:${workspaceInfo.totalSize} bytes` : ''} +
diff --git a/desktop/src/components/VikingPanel.tsx b/desktop/src/components/VikingPanel.tsx index 2d663c1..f44b748 100644 --- a/desktop/src/components/VikingPanel.tsx +++ b/desktop/src/components/VikingPanel.tsx @@ -46,9 +46,9 @@ export function VikingPanel() { setStatus(vikingStatus); if (vikingStatus.available) { - // Load memory count + // Load memory count (use empty path — viking_ls('') returns all, viking_ls('/') returns 0) try { - const resources = await listVikingResources('/'); + const resources = await listVikingResources(''); setMemoryCount(resources.length); } catch { setMemoryCount(null); diff --git a/desktop/src/store/configStore.ts b/desktop/src/store/configStore.ts index 9256812..3a2b4b4 100644 --- a/desktop/src/store/configStore.ts +++ b/desktop/src/store/configStore.ts @@ -450,21 +450,50 @@ export const useConfigStore = create((set loadSkillsCatalog: async () => { const client = get().client; - if (!client) return; - try { - const result = await client.listSkills(); - set({ skillsCatalog: result?.skills || [] }); - if (result?.extraDirs) { - set((state) => ({ - quickConfig: { - ...state.quickConfig, - skillsExtraDirs: result.extraDirs, - }, - })); + // Path A: via injected client (KernelClient or GatewayClient) + if (client) { + try { + const result = await client.listSkills(); + if (result?.skills && result.skills.length > 0) { + set({ skillsCatalog: result.skills }); + if (result.extraDirs) { + set((state) => ({ + quickConfig: { + ...state.quickConfig, + skillsExtraDirs: result.extraDirs, + }, + })); + } + return; + } + } catch (err) { + console.warn('[configStore] listSkills via client failed, trying direct invoke:', err); } - } catch { - // Ignore if skills list not available + } + + // Path B: direct Tauri invoke fallback (works even without client injection) + try { + const skills = await invoke('skill_list'); + if (Array.isArray(skills) && skills.length > 0) { + set({ skillsCatalog: skills.map((s: Record) => ({ + id: s.id as string, + name: s.name as string, + description: (s.description as string) || '', + version: (s.version as string) || '', + capabilities: (s.capabilities as string[]) || [], + tags: (s.tags as string[]) || [], + mode: (s.mode as string) || '', + triggers: ((s.triggers as string[]) || []).map((t: string) => ({ type: 'keyword' as const, pattern: t })), + actions: ((s.capabilities as string[]) || []).map((cap: string) => ({ type: cap, params: undefined })), + enabled: (s.enabled as boolean) ?? true, + category: s.category as string, + source: ((s.source as string) || 'builtin') as 'builtin' | 'extra', + path: s.path as string | undefined, + })) }); + } + } catch (err) { + console.warn('[configStore] skill_list direct invoke also failed:', err); } }, diff --git a/desktop/src/store/securityStore.ts b/desktop/src/store/securityStore.ts index f25071f..91fb3ef 100644 --- a/desktop/src/store/securityStore.ts +++ b/desktop/src/store/securityStore.ts @@ -257,16 +257,64 @@ export const useSecurityStore = create((set, get) => ({ }, loadAuditLogs: async (opts?: { limit?: number; offset?: number }) => { - const client = get().client; - if (!client) return; - set({ auditLogsLoading: true }); + + // Path A: try client (Gateway mode) + const client = get().client; + if (client) { + try { + const result = await client.getAuditLogs(opts); + if (result?.logs && result.logs.length > 0) { + set({ auditLogs: result.logs as AuditLogEntry[], auditLogsLoading: false }); + return; + } + } catch { /* fallback to local */ } + } + + // Path B: read from localStorage (Tauri/Kernel mode) try { - const result = await client.getAuditLogs(opts); - set({ auditLogs: (result?.logs || []) as AuditLogEntry[], auditLogsLoading: false }); + const allLogs: AuditLogEntry[] = []; + + // Source 1: security-audit.ts events (zclaw_security_audit_log) + const securityLog = localStorage.getItem('zclaw_security_audit_log'); + if (securityLog) { + const events = JSON.parse(securityLog) as Array<{ + id: string; timestamp: string; eventType: string; severity: string; + description: string; details?: Record; + }>; + events.forEach(e => allLogs.push({ + id: e.id, + timestamp: e.timestamp, + action: e.eventType, + actor: 'system', + result: e.severity === 'critical' || e.severity === 'high' ? 'failure' : 'success', + details: { description: e.description, ...e.details }, + })); + } + + // Source 2: autonomy audit log (zclaw-autonomy-audit-log) + const autonomyLog = localStorage.getItem('zclaw-autonomy-audit-log'); + if (autonomyLog) { + const events = JSON.parse(autonomyLog) as Array<{ + id: string; timestamp: string; action: string; + decision?: Record; outcome?: string; + }>; + events.forEach(e => allLogs.push({ + id: e.id, + timestamp: e.timestamp, + action: `autonomy.${e.action}`, + actor: 'system', + result: e.outcome === 'success' ? 'success' : 'failure', + details: e.decision || {}, + })); + } + + // Sort by timestamp descending and apply limit + allLogs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + const limited = opts?.limit ? allLogs.slice(0, opts.limit) : allLogs; + set({ auditLogs: limited, auditLogsLoading: false }); } catch { set({ auditLogsLoading: false }); - /* ignore if audit API not available */ } }, }));