feat(desktop): 管家Tab记忆展示增强 — L1摘要+类型分组+用户画像
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
MemorySection.tsx 重写: - 并行加载 L1 摘要 (viking_read L1) 替代仅显示 URI - 按记忆类型分组: 偏好/知识/经验/会话 - 折叠/展开每组,默认展开偏好和知识 - 新增用户画像卡片: 行业/角色/沟通风格/近期话题/常用工具 - 数据源: viking_ls + viking_read + agent_get(userProfile)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Brain, Loader2 } from 'lucide-react';
|
import { Brain, Loader2, ChevronDown, ChevronRight, User } from 'lucide-react';
|
||||||
import { listVikingResources } from '../../lib/viking-client';
|
import { listVikingResources, readVikingResource } from '../../lib/viking-client';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
interface MemorySectionProps {
|
interface MemorySectionProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
@@ -11,29 +12,140 @@ interface MemoryEntry {
|
|||||||
uri: string;
|
uri: string;
|
||||||
name: string;
|
name: string;
|
||||||
resourceType: 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<MemoryGroup, string> = {
|
||||||
|
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<UserProfile | null> {
|
||||||
|
try {
|
||||||
|
const result = await invoke<{ userProfile?: UserProfile } | null>('agent_get', { agentId });
|
||||||
|
return result?.userProfile ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MemorySection({ agentId, refreshKey }: MemorySectionProps) {
|
export function MemorySection({ agentId, refreshKey }: MemorySectionProps) {
|
||||||
const [memories, setMemories] = useState<MemoryEntry[]>([]);
|
const [memories, setMemories] = useState<MemoryEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [expandedGroups, setExpandedGroups] = useState<Set<MemoryGroup>>(new Set(['preferences', 'knowledge']));
|
||||||
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
|
const [_profileLoading, setProfileLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadMemories = useCallback(async () => {
|
||||||
if (!agentId) return;
|
if (!agentId) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// 查询 agent:// 下的所有记忆资源 (preferences/knowledge/experience/sessions)
|
try {
|
||||||
listVikingResources(`agent://${agentId}/`)
|
const entries = await listVikingResources(`agent://${agentId}/`);
|
||||||
.then((entries) => {
|
const typed = entries as MemoryEntry[];
|
||||||
setMemories(entries as MemoryEntry[]);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Memory path may not exist yet — show empty state
|
|
||||||
setMemories([]);
|
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, [agentId, refreshKey]);
|
|
||||||
|
|
||||||
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<Record<MemoryGroup, MemoryEntry[]>>((acc, m) => {
|
||||||
|
const group = classifyGroup(m.resourceType);
|
||||||
|
if (!acc[group]) acc[group] = [];
|
||||||
|
acc[group].push(m);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<MemoryGroup, MemoryEntry[]>);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="w-5 h-5 text-gray-400 animate-spin" />
|
<Loader2 className="w-5 h-5 text-gray-400 animate-spin" />
|
||||||
@@ -41,7 +153,7 @@ export function MemorySection({ agentId, refreshKey }: MemorySectionProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (memories.length === 0) {
|
if (totalMemories === 0 && !hasProfile) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<Brain className="w-8 h-8 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
|
<Brain className="w-8 h-8 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
|
||||||
@@ -54,20 +166,114 @@ export function MemorySection({ agentId, refreshKey }: MemorySectionProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{memories.map((memory) => (
|
{/* User Profile Card */}
|
||||||
<div
|
{hasProfile && (
|
||||||
key={memory.uri}
|
<div className="rounded-lg border border-blue-100 dark:border-blue-900/30 bg-blue-50/50 dark:bg-blue-900/10 px-3 py-2.5">
|
||||||
className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2"
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
>
|
<User className="w-3.5 h-3.5 text-blue-500" />
|
||||||
<div className="text-sm text-gray-900 dark:text-gray-100 truncate">
|
<span className="text-xs font-medium text-blue-700 dark:text-blue-300">用户画像</span>
|
||||||
{memory.name}
|
{profile.confidence !== undefined && profile.confidence > 0 && (
|
||||||
|
<span className="text-[10px] text-blue-400 dark:text-blue-500 ml-auto">
|
||||||
|
置信度 {Math.round(profile.confidence * 100)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 dark:text-gray-500 truncate mt-0.5">
|
<div className="space-y-1.5">
|
||||||
{memory.uri}
|
{profile.industry && (
|
||||||
|
<ProfileField label="行业" value={profile.industry} />
|
||||||
|
)}
|
||||||
|
{profile.role && (
|
||||||
|
<ProfileField label="角色" value={profile.role} />
|
||||||
|
)}
|
||||||
|
{profile.expertise_level && (
|
||||||
|
<ProfileField label="专业水平" value={profile.expertise_level} />
|
||||||
|
)}
|
||||||
|
{profile.communication_style && (
|
||||||
|
<ProfileField label="沟通风格" value={profile.communication_style} />
|
||||||
|
)}
|
||||||
|
{profile.recent_topics && profile.recent_topics.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 items-center">
|
||||||
|
<span className="text-[10px] text-gray-500 dark:text-gray-400 shrink-0">近期话题</span>
|
||||||
|
{profile.recent_topics.slice(0, 8).map((topic) => (
|
||||||
|
<span key={topic} className="inline-block text-[10px] px-1.5 py-0.5 rounded bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-700">
|
||||||
|
{topic}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{profile.preferred_tools && profile.preferred_tools.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 items-center">
|
||||||
|
<span className="text-[10px] text-gray-500 dark:text-gray-400 shrink-0">常用工具</span>
|
||||||
|
{profile.preferred_tools.map((tool) => (
|
||||||
|
<span key={tool} className="inline-block text-[10px] px-1.5 py-0.5 rounded bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400">
|
||||||
|
{tool}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
{/* Memory Groups */}
|
||||||
|
{nonEmptyGroups.map((group) => {
|
||||||
|
const isExpanded = expandedGroups.has(group);
|
||||||
|
const items = grouped[group] ?? [];
|
||||||
|
return (
|
||||||
|
<div key={group}>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleGroup(group)}
|
||||||
|
className="flex items-center gap-1.5 w-full text-left hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded px-1 py-1 transition-colors"
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3.5 h-3.5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{GROUP_LABELS[group]}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-gray-400 dark:text-gray-500">
|
||||||
|
{items.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-1 space-y-1.5 pl-1">
|
||||||
|
{items.map((memory) => (
|
||||||
|
<div
|
||||||
|
key={memory.uri}
|
||||||
|
className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/50 px-3 py-2"
|
||||||
|
>
|
||||||
|
<div className="text-xs text-gray-800 dark:text-gray-200 leading-relaxed">
|
||||||
|
{memory.summary || memory.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-[10px] text-gray-400 dark:text-gray-500">
|
||||||
|
{memory.name}
|
||||||
|
</span>
|
||||||
|
{memory.modifiedAt && (
|
||||||
|
<span className="text-[10px] text-gray-400 dark:text-gray-500 ml-auto">
|
||||||
|
{formatDate(memory.modifiedAt)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileField({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[10px] text-gray-500 dark:text-gray-400 shrink-0 w-14">{label}</span>
|
||||||
|
<span className="text-xs text-gray-700 dark:text-gray-300">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
10
wiki/log.md
10
wiki/log.md
@@ -9,6 +9,16 @@ tags: [log, history]
|
|||||||
|
|
||||||
> Append-only 操作记录。格式: `## [日期] 类型 | 描述`
|
> 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)
|
## [2026-04-22] fix | Agent 搜索功能修复 — glm 空参数 + schema 简化 + 排版修复 (commit 5816f56 + 81005c3)
|
||||||
|
|
||||||
**根因**: glm-5.1 不理解 `oneOf`+`const` 复杂 JSON Schema,tool_calls arguments 始终为空 `{}`。
|
**根因**: glm-5.1 不理解 `oneOf`+`const` 复杂 JSON Schema,tool_calls arguments 始终为空 `{}`。
|
||||||
|
|||||||
Reference in New Issue
Block a user