From 1e65b56a0f120bcaaf268fd551a928b420c29330 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 16 Apr 2026 17:07:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(identity):=203=20=E9=A1=B9=E6=A0=B9?= =?UTF-8?q?=E5=9B=A0=E7=BA=A7=E4=BF=AE=E5=A4=8D=20=E2=80=94=20Agent=20ID?= =?UTF-8?q?=20=E6=98=A0=E5=B0=84=20+=20user=5Fprofile=20=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=20+=20=E7=94=A8=E6=88=B7=E7=94=BB=E5=83=8F=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 2: IdentityFile 枚举补全 UserProfile 变体 - get_file()/propose_change()/approve_proposal() 补全 match arm - identity_get_file/identity_propose_change Tauri 命令支持 user_profile Issue 1: Agent ID 映射机制 - 新增 resolveKernelAgentId() 工具函数 (带缓存) - ButlerPanel 使用 kernel UUID 替代 SaaS relay "1" 查询 VikingStorage Issue 3: 用户画像 fallback 注入 - build_system_prompt 改为 async,identity user_profile 为默认值时 从 VikingStorage preferences 路径查询最近 5 条记忆作为 fallback - intelligence_hooks 调用处同步加 .await Co-Authored-By: Claude Opus 4.6 --- .../src-tauri/src/intelligence/identity.rs | 45 +++++++++++++++++-- desktop/src-tauri/src/intelligence_hooks.rs | 2 +- desktop/src/components/ButlerPanel/index.tsx | 21 +++++++-- desktop/src/lib/kernel-agent.ts | 39 ++++++++++++++++ 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/desktop/src-tauri/src/intelligence/identity.rs b/desktop/src-tauri/src/intelligence/identity.rs index e29ac0e..4941acb 100644 --- a/desktop/src-tauri/src/intelligence/identity.rs +++ b/desktop/src-tauri/src/intelligence/identity.rs @@ -18,6 +18,7 @@ use chrono::Utc; use serde::{Deserialize, Serialize}; +use zclaw_growth::VikingStorage; use std::collections::HashMap; use std::fs; use std::path::PathBuf; @@ -53,6 +54,7 @@ pub struct IdentityChangeProposal { pub enum IdentityFile { Soul, Instructions, + UserProfile, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -270,11 +272,13 @@ impl AgentIdentityManager { match file { IdentityFile::Soul => identity.soul, IdentityFile::Instructions => identity.instructions, + IdentityFile::UserProfile => identity.user_profile, } } - /// Build system prompt from identity files - pub fn build_system_prompt(&mut self, agent_id: &str, memory_context: Option<&str>) -> String { + /// Build system prompt from identity files. + /// Async because it may query VikingStorage as a fallback for user preferences. + pub async fn build_system_prompt(&mut self, agent_id: &str, memory_context: Option<&str>) -> String { let identity = self.get_identity(agent_id); let mut sections = Vec::new(); @@ -300,6 +304,33 @@ impl AgentIdentityManager { if !truncated.is_empty() { sections.push(format!("## 用户画像\n{}", truncated)); } + } else { + // Fallback: query VikingStorage for user-related preferences. + // The UserProfiler pipeline stores extracted preferences under agent://{uuid}/preferences/. + // When identity's user_profile is default (never populated), use this as a data source. + if let Ok(storage) = crate::viking_commands::get_storage().await { + let prefix = format!("agent://{}/preferences/", agent_id); + if let Ok(entries) = storage.find_by_prefix(&prefix).await { + if !entries.is_empty() { + let prefs: Vec = entries + .iter() + .filter_map(|e| { + let text = if e.content.len() > 80 { + let truncated: String = e.content.chars().take(80).collect(); + format!("{}...", truncated) + } else { + e.content.clone() + }; + if text.is_empty() { None } else { Some(format!("- {}", text)) } + }) + .take(5) + .collect(); + if !prefs.is_empty() { + sections.push(format!("## 用户偏好\n{}", prefs.join("\n"))); + } + } + } + } } if let Some(ctx) = memory_context { sections.push(ctx.to_string()); @@ -341,6 +372,7 @@ impl AgentIdentityManager { let current_content = match file { IdentityFile::Soul => identity.soul.clone(), IdentityFile::Instructions => identity.instructions.clone(), + IdentityFile::UserProfile => identity.user_profile.clone(), }; let proposal = IdentityChangeProposal { @@ -386,6 +418,9 @@ impl AgentIdentityManager { IdentityFile::Instructions => { updated.instructions = suggested_content } + IdentityFile::UserProfile => { + updated.user_profile = suggested_content + } } self.identities.insert(agent_id.clone(), updated.clone()); @@ -606,6 +641,7 @@ pub async fn identity_get_file( let file_type = match file.as_str() { "soul" => IdentityFile::Soul, "instructions" => IdentityFile::Instructions, + "userprofile" | "user_profile" => IdentityFile::UserProfile, _ => return Err(format!("Unknown file: {}", file)), }; Ok(manager.get_file(&agent_id, file_type)) @@ -620,7 +656,7 @@ pub async fn identity_build_prompt( state: tauri::State<'_, IdentityManagerState>, ) -> Result { let mut manager = state.lock().await; - Ok(manager.build_system_prompt(&agent_id, memory_context.as_deref())) + Ok(manager.build_system_prompt(&agent_id, memory_context.as_deref()).await) } /// Update user profile (auto) @@ -662,7 +698,8 @@ pub async fn identity_propose_change( let file_type = match target.as_str() { "soul" => IdentityFile::Soul, "instructions" => IdentityFile::Instructions, - _ => return Err(format!("Invalid file type: '{}'. Expected 'soul' or 'instructions'", target)), + "userprofile" | "user_profile" => IdentityFile::UserProfile, + _ => return Err(format!("Invalid file type: '{}'. Expected 'soul', 'instructions', or 'user_profile'", target)), }; Ok(manager.propose_change(&agent_id, file_type, &suggested_content, &reason)) } diff --git a/desktop/src-tauri/src/intelligence_hooks.rs b/desktop/src-tauri/src/intelligence_hooks.rs index 3b0c09d..939f243 100644 --- a/desktop/src-tauri/src/intelligence_hooks.rs +++ b/desktop/src-tauri/src/intelligence_hooks.rs @@ -283,7 +283,7 @@ async fn build_identity_prompt( let prompt = manager.build_system_prompt( agent_id, if memory_context.is_empty() { None } else { Some(memory_context) }, - ); + ).await; Ok(prompt) } diff --git a/desktop/src/components/ButlerPanel/index.tsx b/desktop/src/components/ButlerPanel/index.tsx index f41b4f8..d84886d 100644 --- a/desktop/src/components/ButlerPanel/index.tsx +++ b/desktop/src/components/ButlerPanel/index.tsx @@ -3,6 +3,7 @@ import { useButlerInsights } from '../../hooks/useButlerInsights'; import { useChatStore } from '../../store/chatStore'; import { useIndustryStore } from '../../store/industryStore'; import { extractAndStoreMemories } from '../../lib/viking-client'; +import { resolveKernelAgentId } from '../../lib/kernel-agent'; import { InsightsSection } from './InsightsSection'; import { ProposalsSection } from './ProposalsSection'; import { MemorySection } from './MemorySection'; @@ -17,6 +18,18 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) { const { accountIndustries, configs, lastSynced, isLoading: industryLoading, fetchIndustries } = useIndustryStore(); const [analyzing, setAnalyzing] = useState(false); const [memoryRefreshKey, setMemoryRefreshKey] = useState(0); + const [resolvedAgentId, setResolvedAgentId] = useState(null); + + // Resolve SaaS relay agentId ("1") to kernel UUID for VikingStorage queries + useEffect(() => { + if (!agentId) { + setResolvedAgentId(null); + return; + } + resolveKernelAgentId(agentId) + .then(setResolvedAgentId) + .catch(() => setResolvedAgentId(agentId)); + }, [agentId]); // Auto-fetch industry configs once per session useEffect(() => { @@ -29,7 +42,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) { const canAnalyze = messageCount >= 2; const handleAnalyze = useCallback(async () => { - if (!canAnalyze || analyzing || !agentId) return; + if (!canAnalyze || analyzing || !resolvedAgentId) return; setAnalyzing(true); try { // 1. Refresh pain points & proposals @@ -42,7 +55,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) { role: m.role as 'user' | 'assistant', content: typeof m.content === 'string' ? m.content : '', })); - await extractAndStoreMemories(extractionMessages, agentId); + await extractAndStoreMemories(extractionMessages, resolvedAgentId); // Trigger MemorySection to reload setMemoryRefreshKey((k) => k + 1); } @@ -51,7 +64,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) { } finally { setAnalyzing(false); } - }, [canAnalyze, analyzing, agentId, refresh]); + }, [canAnalyze, analyzing, resolvedAgentId, refresh]); if (!agentId) { return ( @@ -124,7 +137,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {

我记得关于您

- + {/* Industry section */} diff --git a/desktop/src/lib/kernel-agent.ts b/desktop/src/lib/kernel-agent.ts index bef7348..9361f28 100644 --- a/desktop/src/lib/kernel-agent.ts +++ b/desktop/src/lib/kernel-agent.ts @@ -270,3 +270,42 @@ export function installAgentMethods(ClientClass: { prototype: KernelClient }): v return { clone }; }; } + +// === Agent ID Resolution === + +/** + * Cached kernel default agent UUID. + * The conversationStore's DEFAULT_AGENT has id="1", but VikingStorage + * stores data under kernel UUIDs. This cache bridges the gap. + */ +let _cachedDefaultKernelAgentId: string | null = null; + +/** + * Resolve an agent ID to the kernel's actual agent UUID. + * - If already a UUID (8-4-4 hex pattern), return as-is. + * - If "1" or undefined, query agent_list and cache the first kernel agent's UUID. + * - Falls back to the original ID if kernel has no agents. + */ +export async function resolveKernelAgentId(agentId: string | undefined): Promise { + if (agentId && /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(agentId)) { + return agentId; + } + if (_cachedDefaultKernelAgentId) { + return _cachedDefaultKernelAgentId; + } + try { + const agents = await invoke<{ id: string }[]>('agent_list'); + if (agents.length > 0) { + _cachedDefaultKernelAgentId = agents[0].id; + return _cachedDefaultKernelAgentId; + } + } catch { + // Kernel may not be available + } + return agentId || '1'; +} + +/** Invalidate cache when kernel reconnects (new instance may have different UUIDs) */ +export function invalidateKernelAgentIdCache(): void { + _cachedDefaultKernelAgentId = null; +}