diff --git a/crates/zclaw-growth/src/lib.rs b/crates/zclaw-growth/src/lib.rs index e31ddd6..2caae49 100644 --- a/crates/zclaw-growth/src/lib.rs +++ b/crates/zclaw-growth/src/lib.rs @@ -133,7 +133,7 @@ pub use retrieval::{EmbeddingClient, MemoryCache, QueryAnalyzer, SemanticScorer} pub use summarizer::SummaryLlmDriver; pub use experience_extractor::ExperienceExtractor; pub use json_utils::{extract_json_block, extract_string_array}; -pub use profile_updater::{ProfileFieldUpdate, UserProfileUpdater}; +pub use profile_updater::{ProfileFieldUpdate, ProfileUpdateKind, UserProfileUpdater}; pub use pattern_aggregator::{AggregatedPattern, PatternAggregator}; pub use skill_generator::{SkillCandidate, SkillGenerator}; pub use quality_gate::{QualityGate, QualityReport}; diff --git a/crates/zclaw-growth/src/profile_updater.rs b/crates/zclaw-growth/src/profile_updater.rs index 84ce50f..e715125 100644 --- a/crates/zclaw-growth/src/profile_updater.rs +++ b/crates/zclaw-growth/src/profile_updater.rs @@ -4,11 +4,21 @@ use crate::types::CombinedExtraction; +/// 更新类型:字段覆盖 vs 数组追加 +#[derive(Debug, Clone, PartialEq)] +pub enum ProfileUpdateKind { + /// 直接覆盖字段值(industry, communication_style) + SetField, + /// 追加到 JSON 数组字段(recent_topic, pain_point, preferred_tool) + AppendArray, +} + /// 待更新的画像字段 #[derive(Debug, Clone, PartialEq)] pub struct ProfileFieldUpdate { pub field: String, pub value: String, + pub kind: ProfileUpdateKind, } /// 用户画像更新器 @@ -22,11 +32,7 @@ impl UserProfileUpdater { } /// 从提取结果中收集需要更新的画像字段 - /// 返回 (field, value) 列表,由调用方负责实际的异步写入 - /// - /// 注意:只收集 UserProfileStore::update_field() 支持的字段。 - /// ProfileSignals 中的 recent_topic / pain_point / preferred_tool - /// 需要 update_field 扩展后才能写入,当前跳过。 + /// 返回 (field, value, kind) 列表,由调用方根据 kind 选择写入方式 pub fn collect_updates( &self, extraction: &CombinedExtraction, @@ -38,6 +44,7 @@ impl UserProfileUpdater { updates.push(ProfileFieldUpdate { field: "industry".to_string(), value: industry.clone(), + kind: ProfileUpdateKind::SetField, }); } @@ -45,6 +52,31 @@ impl UserProfileUpdater { updates.push(ProfileFieldUpdate { field: "communication_style".to_string(), value: style.clone(), + kind: ProfileUpdateKind::SetField, + }); + } + + if let Some(ref topic) = signals.recent_topic { + updates.push(ProfileFieldUpdate { + field: "recent_topic".to_string(), + value: topic.clone(), + kind: ProfileUpdateKind::AppendArray, + }); + } + + if let Some(ref pain) = signals.pain_point { + updates.push(ProfileFieldUpdate { + field: "pain_point".to_string(), + value: pain.clone(), + kind: ProfileUpdateKind::AppendArray, + }); + } + + if let Some(ref tool) = signals.preferred_tool { + updates.push(ProfileFieldUpdate { + field: "preferred_tool".to_string(), + value: tool.clone(), + kind: ProfileUpdateKind::AppendArray, }); } @@ -73,6 +105,7 @@ mod tests { assert_eq!(updates.len(), 1); assert_eq!(updates[0].field, "industry"); assert_eq!(updates[0].value, "healthcare"); + assert_eq!(updates[0].kind, ProfileUpdateKind::SetField); } #[test] @@ -94,4 +127,31 @@ mod tests { assert_eq!(updates.len(), 2); } + + #[test] + fn test_collect_updates_all_five_dimensions() { + let mut extraction = CombinedExtraction::default(); + extraction.profile_signals.industry = Some("healthcare".to_string()); + extraction.profile_signals.communication_style = Some("concise".to_string()); + extraction.profile_signals.recent_topic = Some("报表自动化".to_string()); + extraction.profile_signals.pain_point = Some("手动汇总太慢".to_string()); + extraction.profile_signals.preferred_tool = Some("researcher".to_string()); + + let updater = UserProfileUpdater::new(); + let updates = updater.collect_updates(&extraction); + + assert_eq!(updates.len(), 5); + let set_fields: Vec<_> = updates + .iter() + .filter(|u| u.kind == ProfileUpdateKind::SetField) + .map(|u| u.field.as_str()) + .collect(); + let append_fields: Vec<_> = updates + .iter() + .filter(|u| u.kind == ProfileUpdateKind::AppendArray) + .map(|u| u.field.as_str()) + .collect(); + assert_eq!(set_fields, vec!["industry", "communication_style"]); + assert_eq!(append_fields, vec!["recent_topic", "pain_point", "preferred_tool"]); + } } diff --git a/crates/zclaw-runtime/src/growth.rs b/crates/zclaw-runtime/src/growth.rs index df14e75..9c6dc08 100644 --- a/crates/zclaw-runtime/src/growth.rs +++ b/crates/zclaw-runtime/src/growth.rs @@ -325,10 +325,40 @@ impl GrowthIntegration { let updates = self.profile_updater.collect_updates(&combined); let user_id = agent_id.to_string(); for update in updates { - if let Err(e) = profile_store - .update_field(&user_id, &update.field, &update.value) - .await - { + let result = match update.kind { + zclaw_growth::ProfileUpdateKind::SetField => { + profile_store + .update_field(&user_id, &update.field, &update.value) + .await + } + zclaw_growth::ProfileUpdateKind::AppendArray => { + match update.field.as_str() { + "recent_topic" => { + profile_store + .add_recent_topic(&user_id, &update.value, 10) + .await + } + "pain_point" => { + profile_store + .add_pain_point(&user_id, &update.value, 10) + .await + } + "preferred_tool" => { + profile_store + .add_preferred_tool(&user_id, &update.value, 10) + .await + } + _ => { + tracing::warn!( + "[GrowthIntegration] Unknown array field: {}", + update.field + ); + Ok(()) + } + } + } + }; + if let Err(e) = result { tracing::warn!( "[GrowthIntegration] Profile update failed for {}: {}", update.field,