fix(identity): 3 项根因级修复 — Agent ID 映射 + user_profile 读取 + 用户画像 fallback
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
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 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use zclaw_growth::VikingStorage;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -53,6 +54,7 @@ pub struct IdentityChangeProposal {
|
|||||||
pub enum IdentityFile {
|
pub enum IdentityFile {
|
||||||
Soul,
|
Soul,
|
||||||
Instructions,
|
Instructions,
|
||||||
|
UserProfile,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -270,11 +272,13 @@ impl AgentIdentityManager {
|
|||||||
match file {
|
match file {
|
||||||
IdentityFile::Soul => identity.soul,
|
IdentityFile::Soul => identity.soul,
|
||||||
IdentityFile::Instructions => identity.instructions,
|
IdentityFile::Instructions => identity.instructions,
|
||||||
|
IdentityFile::UserProfile => identity.user_profile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build system prompt from identity files
|
/// Build system prompt from identity files.
|
||||||
pub fn build_system_prompt(&mut self, agent_id: &str, memory_context: Option<&str>) -> String {
|
/// 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 identity = self.get_identity(agent_id);
|
||||||
let mut sections = Vec::new();
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
@@ -300,6 +304,33 @@ impl AgentIdentityManager {
|
|||||||
if !truncated.is_empty() {
|
if !truncated.is_empty() {
|
||||||
sections.push(format!("## 用户画像\n{}", truncated));
|
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<String> = 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 {
|
if let Some(ctx) = memory_context {
|
||||||
sections.push(ctx.to_string());
|
sections.push(ctx.to_string());
|
||||||
@@ -341,6 +372,7 @@ impl AgentIdentityManager {
|
|||||||
let current_content = match file {
|
let current_content = match file {
|
||||||
IdentityFile::Soul => identity.soul.clone(),
|
IdentityFile::Soul => identity.soul.clone(),
|
||||||
IdentityFile::Instructions => identity.instructions.clone(),
|
IdentityFile::Instructions => identity.instructions.clone(),
|
||||||
|
IdentityFile::UserProfile => identity.user_profile.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let proposal = IdentityChangeProposal {
|
let proposal = IdentityChangeProposal {
|
||||||
@@ -386,6 +418,9 @@ impl AgentIdentityManager {
|
|||||||
IdentityFile::Instructions => {
|
IdentityFile::Instructions => {
|
||||||
updated.instructions = suggested_content
|
updated.instructions = suggested_content
|
||||||
}
|
}
|
||||||
|
IdentityFile::UserProfile => {
|
||||||
|
updated.user_profile = suggested_content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.identities.insert(agent_id.clone(), updated.clone());
|
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() {
|
let file_type = match file.as_str() {
|
||||||
"soul" => IdentityFile::Soul,
|
"soul" => IdentityFile::Soul,
|
||||||
"instructions" => IdentityFile::Instructions,
|
"instructions" => IdentityFile::Instructions,
|
||||||
|
"userprofile" | "user_profile" => IdentityFile::UserProfile,
|
||||||
_ => return Err(format!("Unknown file: {}", file)),
|
_ => return Err(format!("Unknown file: {}", file)),
|
||||||
};
|
};
|
||||||
Ok(manager.get_file(&agent_id, file_type))
|
Ok(manager.get_file(&agent_id, file_type))
|
||||||
@@ -620,7 +656,7 @@ pub async fn identity_build_prompt(
|
|||||||
state: tauri::State<'_, IdentityManagerState>,
|
state: tauri::State<'_, IdentityManagerState>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let mut manager = state.lock().await;
|
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)
|
/// Update user profile (auto)
|
||||||
@@ -662,7 +698,8 @@ pub async fn identity_propose_change(
|
|||||||
let file_type = match target.as_str() {
|
let file_type = match target.as_str() {
|
||||||
"soul" => IdentityFile::Soul,
|
"soul" => IdentityFile::Soul,
|
||||||
"instructions" => IdentityFile::Instructions,
|
"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))
|
Ok(manager.propose_change(&agent_id, file_type, &suggested_content, &reason))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ async fn build_identity_prompt(
|
|||||||
let prompt = manager.build_system_prompt(
|
let prompt = manager.build_system_prompt(
|
||||||
agent_id,
|
agent_id,
|
||||||
if memory_context.is_empty() { None } else { Some(memory_context) },
|
if memory_context.is_empty() { None } else { Some(memory_context) },
|
||||||
);
|
).await;
|
||||||
|
|
||||||
Ok(prompt)
|
Ok(prompt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useButlerInsights } from '../../hooks/useButlerInsights';
|
|||||||
import { useChatStore } from '../../store/chatStore';
|
import { useChatStore } from '../../store/chatStore';
|
||||||
import { useIndustryStore } from '../../store/industryStore';
|
import { useIndustryStore } from '../../store/industryStore';
|
||||||
import { extractAndStoreMemories } from '../../lib/viking-client';
|
import { extractAndStoreMemories } from '../../lib/viking-client';
|
||||||
|
import { resolveKernelAgentId } from '../../lib/kernel-agent';
|
||||||
import { InsightsSection } from './InsightsSection';
|
import { InsightsSection } from './InsightsSection';
|
||||||
import { ProposalsSection } from './ProposalsSection';
|
import { ProposalsSection } from './ProposalsSection';
|
||||||
import { MemorySection } from './MemorySection';
|
import { MemorySection } from './MemorySection';
|
||||||
@@ -17,6 +18,18 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
|||||||
const { accountIndustries, configs, lastSynced, isLoading: industryLoading, fetchIndustries } = useIndustryStore();
|
const { accountIndustries, configs, lastSynced, isLoading: industryLoading, fetchIndustries } = useIndustryStore();
|
||||||
const [analyzing, setAnalyzing] = useState(false);
|
const [analyzing, setAnalyzing] = useState(false);
|
||||||
const [memoryRefreshKey, setMemoryRefreshKey] = useState(0);
|
const [memoryRefreshKey, setMemoryRefreshKey] = useState(0);
|
||||||
|
const [resolvedAgentId, setResolvedAgentId] = useState<string | null>(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
|
// Auto-fetch industry configs once per session
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,7 +42,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
|||||||
const canAnalyze = messageCount >= 2;
|
const canAnalyze = messageCount >= 2;
|
||||||
|
|
||||||
const handleAnalyze = useCallback(async () => {
|
const handleAnalyze = useCallback(async () => {
|
||||||
if (!canAnalyze || analyzing || !agentId) return;
|
if (!canAnalyze || analyzing || !resolvedAgentId) return;
|
||||||
setAnalyzing(true);
|
setAnalyzing(true);
|
||||||
try {
|
try {
|
||||||
// 1. Refresh pain points & proposals
|
// 1. Refresh pain points & proposals
|
||||||
@@ -42,7 +55,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
|||||||
role: m.role as 'user' | 'assistant',
|
role: m.role as 'user' | 'assistant',
|
||||||
content: typeof m.content === 'string' ? m.content : '',
|
content: typeof m.content === 'string' ? m.content : '',
|
||||||
}));
|
}));
|
||||||
await extractAndStoreMemories(extractionMessages, agentId);
|
await extractAndStoreMemories(extractionMessages, resolvedAgentId);
|
||||||
// Trigger MemorySection to reload
|
// Trigger MemorySection to reload
|
||||||
setMemoryRefreshKey((k) => k + 1);
|
setMemoryRefreshKey((k) => k + 1);
|
||||||
}
|
}
|
||||||
@@ -51,7 +64,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setAnalyzing(false);
|
setAnalyzing(false);
|
||||||
}
|
}
|
||||||
}, [canAnalyze, analyzing, agentId, refresh]);
|
}, [canAnalyze, analyzing, resolvedAgentId, refresh]);
|
||||||
|
|
||||||
if (!agentId) {
|
if (!agentId) {
|
||||||
return (
|
return (
|
||||||
@@ -124,7 +137,7 @@ export function ButlerPanel({ agentId }: ButlerPanelProps) {
|
|||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
我记得关于您
|
我记得关于您
|
||||||
</h3>
|
</h3>
|
||||||
<MemorySection agentId={agentId} refreshKey={memoryRefreshKey} />
|
<MemorySection agentId={resolvedAgentId || agentId} refreshKey={memoryRefreshKey} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Industry section */}
|
{/* Industry section */}
|
||||||
|
|||||||
@@ -270,3 +270,42 @@ export function installAgentMethods(ClientClass: { prototype: KernelClient }): v
|
|||||||
return { clone };
|
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<string> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user