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
根因: ProfileUpdater 只处理 industry 和 communication_style 2/5 维度, 跳过 recent_topic、pain_point、preferred_tool。 修复: - ProfileFieldUpdate 添加 kind 字段 (SetField | AppendArray) - collect_updates() 现在处理全部 5 个维度: - industry, communication_style → SetField (直接覆盖) - recent_topic, pain_point, preferred_tool → AppendArray (追加去重) - growth.rs 根据 ProfileUpdateKind 分派到不同的 UserProfileStore 方法: - SetField → update_field() - AppendArray → add_recent_topic() / add_pain_point() / add_preferred_tool() - ProfileUpdateKind re-exported from lib.rs 测试: test_collect_updates_all_five_dimensions 验证 5 个维度 + 2 种更新类型
158 lines
5.2 KiB
Rust
158 lines
5.2 KiB
Rust
//! 用户画像增量更新器
|
||
//! 从 CombinedExtraction 的 profile_signals 提取需要更新的字段
|
||
//! 不额外调用 LLM,纯规则驱动
|
||
|
||
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,
|
||
}
|
||
|
||
/// 用户画像更新器
|
||
/// 从 CombinedExtraction 的 profile_signals 中提取需更新的字段列表
|
||
/// 调用方(zclaw-runtime)负责实际写入 UserProfileStore
|
||
pub struct UserProfileUpdater;
|
||
|
||
impl UserProfileUpdater {
|
||
pub fn new() -> Self {
|
||
Self
|
||
}
|
||
|
||
/// 从提取结果中收集需要更新的画像字段
|
||
/// 返回 (field, value, kind) 列表,由调用方根据 kind 选择写入方式
|
||
pub fn collect_updates(
|
||
&self,
|
||
extraction: &CombinedExtraction,
|
||
) -> Vec<ProfileFieldUpdate> {
|
||
let signals = &extraction.profile_signals;
|
||
let mut updates = Vec::new();
|
||
|
||
if let Some(ref industry) = signals.industry {
|
||
updates.push(ProfileFieldUpdate {
|
||
field: "industry".to_string(),
|
||
value: industry.clone(),
|
||
kind: ProfileUpdateKind::SetField,
|
||
});
|
||
}
|
||
|
||
if let Some(ref style) = signals.communication_style {
|
||
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,
|
||
});
|
||
}
|
||
|
||
updates
|
||
}
|
||
}
|
||
|
||
impl Default for UserProfileUpdater {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_collect_updates_industry() {
|
||
let mut extraction = CombinedExtraction::default();
|
||
extraction.profile_signals.industry = Some("healthcare".to_string());
|
||
|
||
let updater = UserProfileUpdater::new();
|
||
let updates = updater.collect_updates(&extraction);
|
||
|
||
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]
|
||
fn test_collect_updates_no_signals() {
|
||
let extraction = CombinedExtraction::default();
|
||
let updater = UserProfileUpdater::new();
|
||
let updates = updater.collect_updates(&extraction);
|
||
assert!(updates.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn test_collect_updates_multiple_signals() {
|
||
let mut extraction = CombinedExtraction::default();
|
||
extraction.profile_signals.industry = Some("ecommerce".to_string());
|
||
extraction.profile_signals.communication_style = Some("concise".to_string());
|
||
|
||
let updater = UserProfileUpdater::new();
|
||
let updates = updater.collect_updates(&extraction);
|
||
|
||
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"]);
|
||
}
|
||
}
|