fix(growth): Evolution Engine 审计修复 — 7项全部完成
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
HIGH-1: 提取共享 json_utils.rs,skill_generator/workflow_composer 去重
HIGH-2: FeedbackCollector Vec→HashMap,消除 unwrap() panic 风险
HIGH-3: ProfileUpdater 改为 collect_updates() 返回字段列表,
growth.rs 直接 async 调用 update_field(),不再用 no-op 闭包
MEDIUM-1: EvolutionMiddleware 注入后自动 drain,防止重复注入
MEDIUM-2: PatternAggregator tools 提取改为直接收集 context 值
MEDIUM-3: evolution_engine.rs 移除 4 个未使用 imports
MEDIUM-4: workflow_composer parse_response pattern 参数加下划线
MEDIUM-7: SkillCandidate 添加 version 字段(默认=1)
测试: zclaw-growth 128 tests, zclaw-runtime 86 tests, workspace 0 failures
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
//! 用户画像增量更新器
|
||||
//! 从 CombinedExtraction 的 profile_signals 更新 UserProfileStore
|
||||
//! 从 CombinedExtraction 的 profile_signals 提取需要更新的字段
|
||||
//! 不额外调用 LLM,纯规则驱动
|
||||
|
||||
use crate::types::CombinedExtraction;
|
||||
|
||||
/// 待更新的画像字段
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ProfileFieldUpdate {
|
||||
pub field: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// 用户画像更新器
|
||||
/// 接收 CombinedExtraction 中的 profile_signals,通过回调函数更新画像
|
||||
/// 从 CombinedExtraction 的 profile_signals 中提取需更新的字段列表
|
||||
/// 调用方(zclaw-runtime)负责实际写入 UserProfileStore
|
||||
pub struct UserProfileUpdater;
|
||||
|
||||
impl UserProfileUpdater {
|
||||
@@ -13,31 +21,30 @@ impl UserProfileUpdater {
|
||||
Self
|
||||
}
|
||||
|
||||
/// 从提取结果更新用户画像
|
||||
/// profile_store 通过闭包注入,避免 zclaw-growth 依赖 zclaw-memory
|
||||
pub async fn update<F>(
|
||||
/// 从提取结果中收集需要更新的画像字段
|
||||
/// 返回 (field, value) 列表,由调用方负责实际的异步写入
|
||||
pub fn collect_updates(
|
||||
&self,
|
||||
user_id: &str,
|
||||
extraction: &CombinedExtraction,
|
||||
update_fn: F,
|
||||
) -> zclaw_types::Result<()>
|
||||
where
|
||||
F: Fn(&str, &str, &str) -> zclaw_types::Result<()> + Send + Sync,
|
||||
{
|
||||
) -> Vec<ProfileFieldUpdate> {
|
||||
let signals = &extraction.profile_signals;
|
||||
let mut updates = Vec::new();
|
||||
|
||||
if let Some(ref industry) = signals.industry {
|
||||
update_fn(user_id, "industry", industry)?;
|
||||
updates.push(ProfileFieldUpdate {
|
||||
field: "industry".to_string(),
|
||||
value: industry.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref style) = signals.communication_style {
|
||||
update_fn(user_id, "communication_style", style)?;
|
||||
updates.push(ProfileFieldUpdate {
|
||||
field: "communication_style".to_string(),
|
||||
value: style.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// pain_point 和 preferred_tool 使用单独的方法(有去重和容量限制)
|
||||
// 这些通过 GrowthIntegration 中的具体调用处理
|
||||
|
||||
Ok(())
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,60 +57,37 @@ impl Default for UserProfileUpdater {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_industry() {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
let calls_clone = calls.clone();
|
||||
let update_fn = move |uid: &str, field: &str, val: &str| -> zclaw_types::Result<()> {
|
||||
calls_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((uid.to_string(), field.to_string(), val.to_string()));
|
||||
Ok(())
|
||||
};
|
||||
#[test]
|
||||
fn test_collect_updates_industry() {
|
||||
let mut extraction = CombinedExtraction::default();
|
||||
extraction.profile_signals.industry = Some("healthcare".to_string());
|
||||
|
||||
let updater = UserProfileUpdater::new();
|
||||
updater.update("user1", &extraction, update_fn).await.unwrap();
|
||||
let updates = updater.collect_updates(&extraction);
|
||||
|
||||
let locked = calls.lock().unwrap();
|
||||
assert_eq!(locked.len(), 1);
|
||||
assert_eq!(locked[0].1, "industry");
|
||||
assert_eq!(locked[0].2, "healthcare");
|
||||
assert_eq!(updates.len(), 1);
|
||||
assert_eq!(updates[0].field, "industry");
|
||||
assert_eq!(updates[0].value, "healthcare");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_no_signals() {
|
||||
let update_fn =
|
||||
|_: &str, _: &str, _: &str| -> zclaw_types::Result<()> { Ok(()) };
|
||||
#[test]
|
||||
fn test_collect_updates_no_signals() {
|
||||
let extraction = CombinedExtraction::default();
|
||||
let updater = UserProfileUpdater::new();
|
||||
updater.update("user1", &extraction, update_fn).await.unwrap();
|
||||
// No panic = pass
|
||||
let updates = updater.collect_updates(&extraction);
|
||||
assert!(updates.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_multiple_signals() {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
let calls_clone = calls.clone();
|
||||
let update_fn = move |uid: &str, field: &str, val: &str| -> zclaw_types::Result<()> {
|
||||
calls_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((uid.to_string(), field.to_string(), val.to_string()));
|
||||
Ok(())
|
||||
};
|
||||
#[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();
|
||||
updater.update("user1", &extraction, update_fn).await.unwrap();
|
||||
let updates = updater.collect_updates(&extraction);
|
||||
|
||||
let locked = calls.lock().unwrap();
|
||||
assert_eq!(locked.len(), 2);
|
||||
assert_eq!(updates.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user