Compare commits

...

4 Commits

Author SHA1 Message Date
iven
b3f7328778 feat(ui): '我眼中的你' 双源渲染 — 静态Clone + 动态UserProfile
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
- RightPanel 增加 userProfile state + fetch 逻辑
- 对话结束后通过 CustomEvent 触发画像刷新
- UserProfile 字段: 行业/角色/沟通偏好/近期话题
桥接 identity 系统 → 前端面板。
2026-04-11 12:51:28 +08:00
iven
d50d1ab882 feat(kernel): agent_get 返回值扩展 UserProfile 字段
- AgentInfo 增加 user_profile: Option<Value> (serde default)
- SqliteStorage 增加 pool() getter
- agent_get 命令查询 UserProfileStore 填充 user_profile
- 前端 AgentInfo 类型同步更新
复用已有 UserProfileStore,不新增 Tauri 命令。
2026-04-11 12:51:27 +08:00
iven
d974af3042 fix(reflection): 修复 state restore 竞态 — peek+pop 替代直接 pop
根因: pop_restored_state 在 getHistory 读取前删除数据。
修复: 先 peek 非破坏性读取,apply 后再 pop,确保数据可被多次读取。
2026-04-11 12:51:26 +08:00
iven
8a869f6990 fix(reflection): 降低模式检测阈值 5→3/20→15 以产生更多有意义反思
- task/preference/lesson 累积: 5→3
- high-access memories: 3→2
- low-importance: >20 → >15
- 文案微调: "建议清理" → "可考虑清理"
2026-04-11 12:51:25 +08:00
9 changed files with 100 additions and 11 deletions

View File

@@ -41,6 +41,11 @@ pub(crate) struct MemoryRow {
}
impl SqliteStorage {
/// Get a reference to the underlying connection pool
pub fn pool(&self) -> &SqlitePool {
&self.pool
}
/// Create a new SQLite storage at the given path
pub async fn new(path: impl Into<PathBuf>) -> Result<Self> {
let path = path.into();

View File

@@ -85,6 +85,7 @@ impl AgentRegistry {
system_prompt: config.system_prompt.clone(),
temperature: config.temperature,
max_tokens: config.max_tokens,
user_profile: None,
})
}

View File

@@ -171,6 +171,9 @@ pub struct AgentInfo {
pub system_prompt: Option<String>,
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
/// UserProfile from zclaw-memory UserProfileStore (populated on-demand by agent_get)
#[serde(default)]
pub user_profile: Option<serde_json::Value>,
}
impl From<AgentConfig> for AgentInfo {
@@ -189,6 +192,7 @@ impl From<AgentConfig> for AgentInfo {
system_prompt: config.system_prompt,
temperature: config.temperature,
max_tokens: config.max_tokens,
user_profile: None, // Populated on-demand by agent_get command
}
}
}

View File

@@ -390,7 +390,7 @@ impl ReflectionEngine {
// Pattern: Too many tasks accumulating
let task_count = *type_counts.get("task").unwrap_or(&0);
if task_count >= 5 {
if task_count >= 3 {
let evidence: Vec<String> = memories
.iter()
.filter(|m| m.memory_type == "task")
@@ -408,7 +408,7 @@ impl ReflectionEngine {
// Pattern: Strong preference accumulation
let pref_count = *type_counts.get("preference").unwrap_or(&0);
if pref_count >= 5 {
if pref_count >= 3 {
let evidence: Vec<String> = memories
.iter()
.filter(|m| m.memory_type == "preference")
@@ -426,7 +426,7 @@ impl ReflectionEngine {
// Pattern: Many lessons learned
let lesson_count = *type_counts.get("lesson").unwrap_or(&0);
if lesson_count >= 5 {
if lesson_count >= 3 {
let evidence: Vec<String> = memories
.iter()
.filter(|m| m.memory_type == "lesson")
@@ -447,7 +447,7 @@ impl ReflectionEngine {
.iter()
.filter(|m| m.access_count >= 5 && m.importance >= 7)
.collect();
if high_access.len() >= 3 {
if high_access.len() >= 2 {
let evidence: Vec<String> = high_access.iter().take(3).map(|m| m.content.clone()).collect();
patterns.push(PatternObservation {
@@ -460,9 +460,9 @@ impl ReflectionEngine {
// Pattern: Low-importance memories accumulating
let low_importance_count = memories.iter().filter(|m| m.importance <= 3).count();
if low_importance_count > 20 {
if low_importance_count > 15 {
patterns.push(PatternObservation {
observation: format!("有 {} 条低重要性记忆,建议清理", low_importance_count),
observation: format!("有 {} 条低重要性记忆,可考虑清理", low_importance_count),
frequency: low_importance_count,
sentiment: Sentiment::Neutral,
evidence: vec![],
@@ -721,6 +721,18 @@ pub fn pop_restored_result(agent_id: &str) -> Option<ReflectionResult> {
}
}
/// Peek restored state from cache (non-destructive read)
pub fn peek_restored_state(agent_id: &str) -> Option<ReflectionState> {
let cache = get_state_cache();
cache.read().ok()?.get(agent_id).cloned()
}
/// Peek restored result from cache (non-destructive read)
pub fn peek_restored_result(agent_id: &str) -> Option<ReflectionResult> {
let cache = get_result_cache();
cache.read().ok()?.get(agent_id).cloned()
}
// === Tauri Commands ===
use tokio::sync::Mutex;

View File

@@ -112,12 +112,15 @@ pub async fn post_conversation_hook(
// Step 2: Record conversation for reflection
let mut engine = reflection_state.lock().await;
// Apply restored state on first call (one-shot after app restart)
if let Some(restored_state) = crate::intelligence::reflection::pop_restored_state(agent_id) {
// Apply restored state on first call (peek-then-pop to avoid race with getHistory)
if let Some(restored_state) = crate::intelligence::reflection::peek_restored_state(agent_id) {
engine.apply_restored_state(restored_state);
// Pop after successful apply to prevent re-processing
crate::intelligence::reflection::pop_restored_state(agent_id);
}
if let Some(restored_result) = crate::intelligence::reflection::pop_restored_result(agent_id) {
if let Some(restored_result) = crate::intelligence::reflection::peek_restored_result(agent_id) {
engine.apply_restored_result(restored_result);
crate::intelligence::reflection::pop_restored_result(agent_id);
}
engine.record_conversation();

View File

@@ -163,7 +163,7 @@ pub async fn agent_list(
Ok(kernel.list_agents())
}
/// Get agent info
/// Get agent info (with optional UserProfile from memory store)
// @connected
#[tauri::command]
pub async fn agent_get(
@@ -180,7 +180,19 @@ pub async fn agent_get(
let id: AgentId = agent_id.parse()
.map_err(|_| "Invalid agent ID format".to_string())?;
Ok(kernel.get_agent(&id))
let mut info = kernel.get_agent(&id);
// Extend with UserProfile if available
if let Some(ref mut agent_info) = info {
if let Ok(storage) = crate::viking_commands::get_storage().await {
let profile_store = zclaw_memory::UserProfileStore::new(storage.pool().clone());
if let Ok(Some(profile)) = profile_store.get(&agent_id).await {
agent_info.user_profile = Some(serde_json::to_value(profile).unwrap_or_default());
}
}
}
Ok(info)
}
/// Delete an agent

View File

@@ -8,6 +8,8 @@ import { useConfigStore } from '../store/configStore';
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
import { intelligenceClient, type IdentitySnapshot } from '../lib/intelligence-client';
import { invoke } from '@tauri-apps/api/core';
import type { AgentInfo } from '../lib/kernel-types';
import {
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
MessageSquare, Cpu, FileText, User, Activity, Brain,
@@ -143,6 +145,9 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
const [restoringSnapshotId, setRestoringSnapshotId] = useState<string | null>(null);
const [confirmRestoreId, setConfirmRestoreId] = useState<string | null>(null);
// UserProfile from memory store (dynamic, learned from conversations)
const [userProfile, setUserProfile] = useState<Record<string, unknown> | null>(null);
const connected = connectionState === 'connected';
const selectedClone = useMemo(
() => clones.find((clone) => clone.id === currentAgent?.id),
@@ -166,6 +171,28 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
}
}, [connected]);
// Fetch UserProfile from agent data (includes memory-learned profile)
useEffect(() => {
if (!currentAgent?.id) return;
invoke<AgentInfo | null>('agent_get', { agentId: currentAgent.id })
.then(data => setUserProfile(data?.userProfile ?? null))
.catch(() => setUserProfile(null));
}, [currentAgent?.id]);
// Listen for profile updates after conversations
useEffect(() => {
const handler = (e: Event) => {
const detail = (e as CustomEvent).detail;
if (detail?.agentId === currentAgent?.id && currentAgent?.id) {
invoke<AgentInfo | null>('agent_get', { agentId: currentAgent.id })
.then(data => setUserProfile(data?.userProfile ?? null))
.catch(() => {});
}
};
window.addEventListener('zclaw:agent-profile-updated', handler);
return () => window.removeEventListener('zclaw:agent-profile-updated', handler);
}, [currentAgent?.id]);
const handleReconnect = () => {
connect().catch(silentErrorHandler('RightPanel'));
};
@@ -552,6 +579,24 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
<AgentRow label="已解析" value={selectedClone?.workspaceResolvedPath || workspaceInfo?.resolvedPath || '-'} />
<AgentRow label="文件限制" value={selectedClone?.restrictFiles ? '已开启' : '已关闭'} />
<AgentRow label="隐私计划" value={selectedClone?.privacyOptIn ? '已加入' : '未加入'} />
{/* Dynamic: UserProfile data (from conversation learning) */}
{userProfile && (
<div className="mt-3 pt-3 border-t border-gray-100 dark:border-gray-800">
<div className="text-xs text-gray-400 mb-2"></div>
{userProfile.industry ? (
<AgentRow label="行业" value={String(userProfile.industry)} />
) : null}
{userProfile.role ? (
<AgentRow label="角色" value={String(userProfile.role)} />
) : null}
{userProfile.communicationStyle ? (
<AgentRow label="沟通偏好" value={String(userProfile.communicationStyle)} />
) : null}
{Array.isArray(userProfile.recentTopics) && (userProfile.recentTopics as string[]).length > 0 ? (
<AgentRow label="近期话题" value={(userProfile.recentTopics as string[]).slice(0, 5).join(', ')} />
) : null}
</div>
)}
</div>
)}
</motion.div>

View File

@@ -33,6 +33,7 @@ export interface AgentInfo {
systemPrompt?: string;
temperature?: number;
maxTokens?: number;
userProfile?: Record<string, unknown>;
}
export interface CreateAgentRequest {

View File

@@ -487,6 +487,12 @@ export const useStreamStore = create<StreamState>()(
getMemoryExtractor().extractFromConversation(filtered, agentId, convId ?? undefined).catch(err => {
log.warn('Memory extraction failed:', err);
});
// Notify RightPanel to refresh UserProfile after memory extraction
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('zclaw:agent-profile-updated', {
detail: { agentId }
}));
}
intelligenceClient.reflection.recordConversation().catch(err => {
log.warn('Recording conversation failed:', err);
});