diff --git a/desktop/src/components/ButlerPanel/MemorySection.tsx b/desktop/src/components/ButlerPanel/MemorySection.tsx index 9d7f99d..cb86cf3 100644 --- a/desktop/src/components/ButlerPanel/MemorySection.tsx +++ b/desktop/src/components/ButlerPanel/MemorySection.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; -import { Brain, Loader2 } from 'lucide-react'; -import { listVikingResources } from '../../lib/viking-client'; +import { useState, useEffect, useCallback } from 'react'; +import { Brain, Loader2, ChevronDown, ChevronRight, User } from 'lucide-react'; +import { listVikingResources, readVikingResource } from '../../lib/viking-client'; +import { invoke } from '@tauri-apps/api/core'; interface MemorySectionProps { agentId: string; @@ -11,29 +12,140 @@ interface MemoryEntry { uri: string; name: string; resourceType: string; + size?: number; + modifiedAt?: string; + summary?: string; + loading?: boolean; +} + +type MemoryGroup = 'preferences' | 'knowledge' | 'experience' | 'sessions' | 'other'; + +interface UserProfile { + industry?: string; + role?: string; + expertise_level?: string; + communication_style?: string; + preferred_language?: string; + recent_topics?: string[]; + active_pain_points?: string[]; + preferred_tools?: string[]; + confidence?: number; +} + +const GROUP_LABELS: Record = { + preferences: '偏好', + knowledge: '知识', + experience: '经验', + sessions: '会话', + other: '其他', +}; + +const GROUP_ORDER: MemoryGroup[] = ['preferences', 'knowledge', 'experience', 'sessions', 'other']; + +function classifyGroup(resourceType: string): MemoryGroup { + if (resourceType in GROUP_LABELS) return resourceType as MemoryGroup; + return 'other'; +} + +function formatDate(iso?: string): string { + if (!iso) return ''; + try { + return new Date(iso).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); + } catch { + return ''; + } +} + +// Fetch user profile from agent_get Tauri command +async function fetchUserProfile(agentId: string): Promise { + try { + const result = await invoke<{ userProfile?: UserProfile } | null>('agent_get', { agentId }); + return result?.userProfile ?? null; + } catch { + return null; + } } export function MemorySection({ agentId, refreshKey }: MemorySectionProps) { const [memories, setMemories] = useState([]); const [loading, setLoading] = useState(false); + const [expandedGroups, setExpandedGroups] = useState>(new Set(['preferences', 'knowledge'])); + const [profile, setProfile] = useState(null); + const [_profileLoading, setProfileLoading] = useState(false); - useEffect(() => { + const loadMemories = useCallback(async () => { if (!agentId) return; setLoading(true); - // 查询 agent:// 下的所有记忆资源 (preferences/knowledge/experience/sessions) - listVikingResources(`agent://${agentId}/`) - .then((entries) => { - setMemories(entries as MemoryEntry[]); - }) - .catch(() => { - // Memory path may not exist yet — show empty state - setMemories([]); - }) - .finally(() => setLoading(false)); - }, [agentId, refreshKey]); + try { + const entries = await listVikingResources(`agent://${agentId}/`); + const typed = entries as MemoryEntry[]; - if (loading) { + // Load L1 summaries in parallel (batched to avoid overwhelming) + const enriched = await Promise.all( + typed.map(async (entry) => { + try { + const summary = await readVikingResource(entry.uri, 'L1'); + return { ...entry, summary: summary || entry.name }; + } catch { + return { ...entry, summary: entry.name }; + } + }) + ); + + setMemories(enriched); + } catch { + setMemories([]); + } finally { + setLoading(false); + } + }, [agentId]); + + const loadProfile = useCallback(async () => { + if (!agentId) return; + setProfileLoading(true); + try { + const p = await fetchUserProfile(agentId); + setProfile(p); + } catch { + setProfile(null); + } finally { + setProfileLoading(false); + } + }, [agentId]); + + useEffect(() => { + loadMemories(); + loadProfile(); + }, [loadMemories, loadProfile, refreshKey]); + + // Group memories by type + const grouped = memories.reduce>((acc, m) => { + const group = classifyGroup(m.resourceType); + if (!acc[group]) acc[group] = []; + acc[group].push(m); + return acc; + }, {} as Record); + + const nonEmptyGroups = GROUP_ORDER.filter((g) => (grouped[g]?.length ?? 0) > 0); + const totalMemories = memories.length; + + const toggleGroup = (group: MemoryGroup) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(group)) next.delete(group); + else next.add(group); + return next; + }); + }; + + const hasProfile = profile && ( + profile.industry || profile.role || profile.communication_style || + (profile.recent_topics && profile.recent_topics.length > 0) || + (profile.preferred_tools && profile.preferred_tools.length > 0) + ); + + if (loading && memories.length === 0) { return (
@@ -41,7 +153,7 @@ export function MemorySection({ agentId, refreshKey }: MemorySectionProps) { ); } - if (memories.length === 0) { + if (totalMemories === 0 && !hasProfile) { return (
@@ -54,20 +166,114 @@ export function MemorySection({ agentId, refreshKey }: MemorySectionProps) { } return ( -
- {memories.map((memory) => ( -
-
- {memory.name} +
+ {/* User Profile Card */} + {hasProfile && ( +
+
+ + 用户画像 + {profile.confidence !== undefined && profile.confidence > 0 && ( + + 置信度 {Math.round(profile.confidence * 100)}% + + )}
-
- {memory.uri} +
+ {profile.industry && ( + + )} + {profile.role && ( + + )} + {profile.expertise_level && ( + + )} + {profile.communication_style && ( + + )} + {profile.recent_topics && profile.recent_topics.length > 0 && ( +
+ 近期话题 + {profile.recent_topics.slice(0, 8).map((topic) => ( + + {topic} + + ))} +
+ )} + {profile.preferred_tools && profile.preferred_tools.length > 0 && ( +
+ 常用工具 + {profile.preferred_tools.map((tool) => ( + + {tool} + + ))} +
+ )}
- ))} + )} + + {/* Memory Groups */} + {nonEmptyGroups.map((group) => { + const isExpanded = expandedGroups.has(group); + const items = grouped[group] ?? []; + return ( +
+ + {isExpanded && ( +
+ {items.map((memory) => ( +
+
+ {memory.summary || memory.name} +
+
+ + {memory.name} + + {memory.modifiedAt && ( + + {formatDate(memory.modifiedAt)} + + )} +
+
+ ))} +
+ )} +
+ ); + })} +
+ ); +} + +function ProfileField({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value}
); } diff --git a/wiki/log.md b/wiki/log.md index fa532c9..c705bff 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -9,6 +9,16 @@ tags: [log, history] > Append-only 操作记录。格式: `## [日期] 类型 | 描述` +### 2026-04-22 管家Tab记忆展示增强 +- **变更**: MemorySection.tsx 重写 — L1摘要并行加载 + 按类型分组(偏好/知识/经验/会话) + 用户画像卡片(行业/角色/沟通风格/近期话题) +- **数据源**: viking_ls+viking_read(L1) + agent_get(userProfile) + +### 2026-04-22 DataMasking 完全移除 +- **变更**: 删除 `data_masking.rs` (367行) + loop_runner unmask 逻辑 + saas-relay-client.ts 前端 mask/unmask +- **原因**: 正则过度匹配中文文本(commit 73d50fd 已禁用),NLP方案未排期,彻底移除减少维护负担 +- **影响**: 中间件链 15→14 层,loop_runner 简化,SaaS relay 路径不再做前端脱敏 +- **中间件链**: `ButlerRouter@80, Compaction@100, Memory@150, Title@180, SkillIndex@200, DanglingTool@300, ToolError@350, ToolOutputGuard@360, Guardrail@400, LoopGuard@500, SubagentLimit@550, TrajectoryRecorder@650, TokenCalibration@700` + ## [2026-04-22] fix | Agent 搜索功能修复 — glm 空参数 + schema 简化 + 排版修复 (commit 5816f56 + 81005c3) **根因**: glm-5.1 不理解 `oneOf`+`const` 复杂 JSON Schema,tool_calls arguments 始终为空 `{}`。