fix(identity): 接通身份信号提取与持久化 — 对话中起名跨会话记忆
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

根因: 记忆提取管道(COMBINED_EXTRACTION_PROMPT)提取5种画像信号
但无身份信号(agent_name/user_name),不存在从对话到AgentConfig.name
或IdentityFiles的写回路径。

修复内容:
- ProfileSignals 增加 agent_name/user_name 字段
- COMBINED_EXTRACTION_PROMPT 增加身份提取指令
- parse_profile_signals 解析新字段 + 回退推断
- GrowthIntegration 存储身份信号到 VikingStorage
- post_conversation_hook 写回 soul.md + emit Tauri 事件
- streamStore 规则化检测 agent 名字并更新 AgentConfig.name
- cold-start-mapper 新增 detectAgentNameSuggestion

链路: 对话→提取→VikingStorage→hook写回soul.md→事件→前端刷新
This commit is contained in:
iven
2026-04-23 09:20:35 +08:00
parent 17a7a36608
commit 08812e541c
7 changed files with 431 additions and 22 deletions

View File

@@ -8,6 +8,8 @@
use tracing::{debug, warn};
use std::sync::Arc;
use tauri::Emitter;
use zclaw_growth::VikingStorage;
use crate::intelligence::identity::IdentityManagerState;
use crate::intelligence::heartbeat::HeartbeatEngineState;
@@ -56,12 +58,15 @@ pub async fn pre_conversation_hook(
///
/// 1. Record interaction for heartbeat engine
/// 2. Record conversation for reflection engine, trigger reflection if needed
/// 3. Detect identity signals and write back to identity files
pub async fn post_conversation_hook(
agent_id: &str,
_user_message: &str,
_heartbeat_state: &HeartbeatEngineState,
reflection_state: &ReflectionEngineState,
llm_driver: Option<Arc<dyn LlmDriver>>,
identity_state: &IdentityManagerState,
app: &tauri::AppHandle,
) {
// Step 1: Record interaction for heartbeat
crate::intelligence::heartbeat::record_interaction(agent_id);
@@ -200,6 +205,71 @@ pub async fn post_conversation_hook(
reflection_result.improvements.len()
);
}
// Step 3: Detect identity signals from recent memory extraction and write back
if let Ok(storage) = crate::viking_commands::get_storage().await {
let identity_prefix = format!("agent://{}/identity/", agent_id);
// Check for agent_name identity signal
let agent_name_uri = format!("{}agent-name", identity_prefix);
if let Ok(Some(entry)) = VikingStorage::get(storage.as_ref(), &agent_name_uri).await {
// Extract name from content like "助手的名字是小马"
let name = entry.content.strip_prefix("助手的名字是")
.map(|n| n.trim().to_string())
.unwrap_or_else(|| entry.content.clone());
if !name.is_empty() {
// Update IdentityFiles.soul to include the agent name
let mut manager = identity_state.lock().await;
let current_soul = manager.get_file(agent_id, crate::intelligence::identity::IdentityFile::Soul);
// Only update if the name isn't already in the soul
if !current_soul.contains(&name) {
let updated_soul = if current_soul.is_empty() {
format!("# ZCLAW 人格\n\n你的名字是{}\n\n你是一个成长性的中文 AI 助手。", name)
} else if current_soul.contains("你的名字是") || current_soul.contains("你的名字:") {
// Replace existing name line
let re = regex::Regex::new(r"你的名字是[^\n]+").unwrap();
re.replace(&current_soul, format!("你的名字是{}", name)).to_string()
} else {
// Prepend name to existing soul
format!("你的名字是{}\n\n{}", name, current_soul)
};
if let Err(e) = manager.update_file(agent_id, "soul", &updated_soul) {
warn!("[intelligence_hooks] Failed to update soul with agent name: {}", e);
} else {
debug!("[intelligence_hooks] Updated agent name to '{}' in soul", name);
}
}
drop(manager);
// Emit event for frontend to update AgentConfig.name
let _ = app.emit("zclaw:agent-identity-updated", serde_json::json!({
"agentId": agent_id,
"agentName": name,
}));
}
}
// Check for user_name identity signal
let user_name_uri = format!("{}user-name", identity_prefix);
if let Ok(Some(entry)) = VikingStorage::get(storage.as_ref(), &user_name_uri).await {
let name = entry.content.strip_prefix("用户的名字是")
.map(|n| n.trim().to_string())
.unwrap_or_else(|| entry.content.clone());
if !name.is_empty() {
let mut manager = identity_state.lock().await;
let profile = manager.get_file(agent_id, crate::intelligence::identity::IdentityFile::UserProfile);
if !profile.contains(&name) {
manager.append_to_user_profile(agent_id, &format!("- 用户名字: {}", name));
debug!("[intelligence_hooks] Appended user name '{}' to profile", name);
}
}
}
}
}
/// Build memory context by searching VikingStorage for relevant memories

View File

@@ -324,6 +324,7 @@ pub async fn agent_chat_stream(
let hb_state = heartbeat_state.inner().clone();
let rf_state = reflection_state.inner().clone();
let id_state_hook = identity_state.inner().clone();
// Clone the guard map for cleanup in the spawned task
let guard_map: SessionStreamGuard = stream_guard.inner().clone();
@@ -380,12 +381,14 @@ pub async fn agent_chat_stream(
let hb = hb_state.clone();
let rf = rf_state.clone();
let driver = llm_driver.clone();
let id_state = id_state_hook.clone();
let app_hook = app.clone();
if driver.is_none() {
tracing::debug!("[agent_chat_stream] Post-hook firing without LLM driver (schedule intercept path)");
}
tokio::spawn(async move {
crate::intelligence_hooks::post_conversation_hook(
&agent_id_hook, &message_hook, &hb, &rf, driver,
&agent_id_hook, &message_hook, &hb, &rf, driver, &id_state, &app_hook,
).await;
});
}