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

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:
iven
2026-04-18 22:15:43 +08:00
parent f97e6fdbb6
commit a9ea9d8691
12 changed files with 163 additions and 175 deletions

1
Cargo.lock generated
View File

@@ -9876,6 +9876,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"ureq", "ureq",
"url",
"uuid", "uuid",
"wasmtime", "wasmtime",
"wasmtime-wasi", "wasmtime-wasi",

View File

@@ -9,8 +9,7 @@ use std::sync::Arc;
use crate::experience_store::ExperienceStore; use crate::experience_store::ExperienceStore;
use crate::feedback_collector::{ use crate::feedback_collector::{
EvolutionArtifact, FeedbackCollector, FeedbackEntry, FeedbackSignal, RecommendedAction, FeedbackCollector, FeedbackEntry, TrustUpdate,
Sentiment, TrustUpdate,
}; };
use crate::pattern_aggregator::{AggregatedPattern, PatternAggregator}; use crate::pattern_aggregator::{AggregatedPattern, PatternAggregator};
use crate::quality_gate::{QualityGate, QualityReport}; use crate::quality_gate::{QualityGate, QualityReport};

View File

@@ -2,6 +2,8 @@
//! 收集用户对进化产物(技能/Pipeline的显式/隐式反馈 //! 收集用户对进化产物(技能/Pipeline的显式/隐式反馈
//! 管理信任度衰减和优化循环 //! 管理信任度衰减和优化循环
use std::collections::HashMap;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -58,21 +60,32 @@ pub struct TrustRecord {
/// 反馈收集器 /// 反馈收集器
/// 管理反馈记录和信任度评分 /// 管理反馈记录和信任度评分
/// 内存存储,可持久化到 SQLite后续版本
pub struct FeedbackCollector { pub struct FeedbackCollector {
/// 信任度记录表(内存,可持久化到 SQLite trust_records: HashMap<String, TrustRecord>,
trust_records: Vec<TrustRecord>,
} }
impl FeedbackCollector { impl FeedbackCollector {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
trust_records: Vec::new(), trust_records: HashMap::new(),
} }
} }
/// 提交一条反馈 /// 提交一条反馈
pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate { pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate {
let record = self.get_or_create_record(&entry.artifact_id, &entry.artifact_type); let record = self
.trust_records
.entry(entry.artifact_id.clone())
.or_insert_with(|| TrustRecord {
artifact_id: entry.artifact_id.clone(),
artifact_type: entry.artifact_type.clone(),
trust_score: 0.5,
total_feedback: 0,
positive_count: 0,
negative_count: 0,
last_updated: Utc::now(),
});
// 更新计数 // 更新计数
record.total_feedback += 1; record.total_feedback += 1;
@@ -106,13 +119,13 @@ impl FeedbackCollector {
/// 获取信任度记录 /// 获取信任度记录
pub fn get_trust(&self, artifact_id: &str) -> Option<&TrustRecord> { pub fn get_trust(&self, artifact_id: &str) -> Option<&TrustRecord> {
self.trust_records.iter().find(|r| r.artifact_id == artifact_id) self.trust_records.get(artifact_id)
} }
/// 获取所有需要优化的产物(信任度 < 0.4 /// 获取所有需要优化的产物(信任度 < 0.4
pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> { pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> {
self.trust_records self.trust_records
.iter() .values()
.filter(|r| r.trust_score < 0.4 && r.total_feedback >= 2) .filter(|r| r.trust_score < 0.4 && r.total_feedback >= 2)
.collect() .collect()
} }
@@ -120,7 +133,7 @@ impl FeedbackCollector {
/// 获取所有应该归档的产物(信任度 < 0.2 且反馈 >= 5 /// 获取所有应该归档的产物(信任度 < 0.2 且反馈 >= 5
pub fn get_artifacts_to_archive(&self) -> Vec<&TrustRecord> { pub fn get_artifacts_to_archive(&self) -> Vec<&TrustRecord> {
self.trust_records self.trust_records
.iter() .values()
.filter(|r| r.trust_score < 0.2 && r.total_feedback >= 5) .filter(|r| r.trust_score < 0.2 && r.total_feedback >= 5)
.collect() .collect()
} }
@@ -128,37 +141,11 @@ impl FeedbackCollector {
/// 获取所有高信任产物(信任度 >= 0.8 /// 获取所有高信任产物(信任度 >= 0.8
pub fn get_recommended_artifacts(&self) -> Vec<&TrustRecord> { pub fn get_recommended_artifacts(&self) -> Vec<&TrustRecord> {
self.trust_records self.trust_records
.iter() .values()
.filter(|r| r.trust_score >= 0.8) .filter(|r| r.trust_score >= 0.8)
.collect() .collect()
} }
fn get_or_create_record(
&mut self,
artifact_id: &str,
artifact_type: &EvolutionArtifact,
) -> &mut TrustRecord {
let exists = self
.trust_records
.iter()
.any(|r| r.artifact_id == artifact_id);
if !exists {
self.trust_records.push(TrustRecord {
artifact_id: artifact_id.to_string(),
artifact_type: artifact_type.clone(),
trust_score: 0.5, // 初始信任度
total_feedback: 0,
positive_count: 0,
negative_count: 0,
last_updated: Utc::now(),
});
}
self.trust_records
.iter_mut()
.find(|r| r.artifact_id == artifact_id)
.unwrap()
}
fn calculate_trust_internal( fn calculate_trust_internal(
positive: u32, positive: u32,
negative: u32, negative: u32,
@@ -268,7 +255,6 @@ mod tests {
#[test] #[test]
fn test_recommend_optimize() { fn test_recommend_optimize() {
let mut collector = FeedbackCollector::new(); let mut collector = FeedbackCollector::new();
// 2 negative → trust < 0.4
collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative)); collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
let update = collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative)); let update = collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
assert_eq!(update.action, RecommendedAction::Optimize); assert_eq!(update.action, RecommendedAction::Optimize);

View File

@@ -0,0 +1,63 @@
//! 共享 JSON 工具函数
//! 从 LLM 返回的文本中提取 JSON 块
/// 从 LLM 返回文本中提取 JSON 块
/// 支持三种格式:```json...``` 围栏、```...``` 围栏、裸 {...}
pub fn extract_json_block(text: &str) -> &str {
// 尝试匹配 ```json ... ```
if let Some(start) = text.find("```json") {
let json_start = start + 7;
if let Some(end) = text[json_start..].find("```") {
return text[json_start..json_start + end].trim();
}
}
// 尝试匹配 ``` ... ```
if let Some(start) = text.find("```") {
let json_start = start + 3;
if let Some(end) = text[json_start..].find("```") {
return text[json_start..json_start + end].trim();
}
}
// 尝试找 { ... } 块
if let Some(start) = text.find('{') {
if let Some(end) = text.rfind('}') {
return &text[start..=end];
}
}
text.trim()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_block_with_markdown() {
let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone.";
assert_eq!(extract_json_block(text), "{\"key\": \"value\"}");
}
#[test]
fn test_json_block_bare() {
let text = "{\"key\": \"value\"}";
assert_eq!(extract_json_block(text), "{\"key\": \"value\"}");
}
#[test]
fn test_json_block_plain_fences() {
let text = "Result:\n```\n{\"a\": 1}\n```";
assert_eq!(extract_json_block(text), "{\"a\": 1}");
}
#[test]
fn test_json_block_nested_braces() {
let text = r#"{"outer": {"inner": "val"}}"#;
assert_eq!(extract_json_block(text), r#"{"outer": {"inner": "val"}}"#);
}
#[test]
fn test_json_block_no_json() {
let text = "no json here";
assert_eq!(extract_json_block(text), "no json here");
}
}

View File

@@ -65,6 +65,7 @@ pub mod storage;
pub mod retrieval; pub mod retrieval;
pub mod summarizer; pub mod summarizer;
pub mod experience_store; pub mod experience_store;
pub mod json_utils;
pub mod experience_extractor; pub mod experience_extractor;
pub mod profile_updater; pub mod profile_updater;
pub mod pattern_aggregator; pub mod pattern_aggregator;
@@ -106,7 +107,8 @@ pub use experience_store::{Experience, ExperienceStore};
pub use retrieval::{EmbeddingClient, MemoryCache, QueryAnalyzer, SemanticScorer}; pub use retrieval::{EmbeddingClient, MemoryCache, QueryAnalyzer, SemanticScorer};
pub use summarizer::SummaryLlmDriver; pub use summarizer::SummaryLlmDriver;
pub use experience_extractor::ExperienceExtractor; pub use experience_extractor::ExperienceExtractor;
pub use profile_updater::UserProfileUpdater; pub use json_utils::extract_json_block;
pub use profile_updater::{ProfileFieldUpdate, UserProfileUpdater};
pub use pattern_aggregator::{AggregatedPattern, PatternAggregator}; pub use pattern_aggregator::{AggregatedPattern, PatternAggregator};
pub use skill_generator::{SkillCandidate, SkillGenerator}; pub use skill_generator::{SkillCandidate, SkillGenerator};
pub use quality_gate::{QualityGate, QualityReport}; pub use quality_gate::{QualityGate, QualityReport};

View File

@@ -52,15 +52,12 @@ impl PatternAggregator {
let total_reuse: u32 = experiences.iter().map(|e| e.reuse_count).sum(); let total_reuse: u32 = experiences.iter().map(|e| e.reuse_count).sum();
let common_steps = Self::find_common_steps(&experiences); let common_steps = Self::find_common_steps(&experiences);
// 从 context 字段提取工具名 // 从 context 字段提取工具名context 存储的是触发工具/来源标识)
// Experience 结构没有独立的 tools 字段context 作为来源标识使用
let tools: Vec<String> = experiences let tools: Vec<String> = experiences
.iter() .iter()
.flat_map(|e| { .map(|e| e.context.clone())
e.context
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
})
.collect::<std::collections::HashSet<_>>() .collect::<std::collections::HashSet<_>>()
.into_iter() .into_iter()
.collect(); .collect();

View File

@@ -1,11 +1,19 @@
//! 用户画像增量更新器 //! 用户画像增量更新器
//! 从 CombinedExtraction 的 profile_signals 更新 UserProfileStore //! 从 CombinedExtraction 的 profile_signals 提取需要更新的字段
//! 不额外调用 LLM纯规则驱动 //! 不额外调用 LLM纯规则驱动
use crate::types::CombinedExtraction; 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; pub struct UserProfileUpdater;
impl UserProfileUpdater { impl UserProfileUpdater {
@@ -13,31 +21,30 @@ impl UserProfileUpdater {
Self Self
} }
/// 从提取结果更新用户画像 /// 从提取结果中收集需要更新的画像字段
/// profile_store 通过闭包注入,避免 zclaw-growth 依赖 zclaw-memory /// 返回 (field, value) 列表,由调用方负责实际的异步写入
pub async fn update<F>( pub fn collect_updates(
&self, &self,
user_id: &str,
extraction: &CombinedExtraction, extraction: &CombinedExtraction,
update_fn: F, ) -> Vec<ProfileFieldUpdate> {
) -> zclaw_types::Result<()>
where
F: Fn(&str, &str, &str) -> zclaw_types::Result<()> + Send + Sync,
{
let signals = &extraction.profile_signals; let signals = &extraction.profile_signals;
let mut updates = Vec::new();
if let Some(ref industry) = signals.industry { 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 { 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 使用单独的方法(有去重和容量限制) updates
// 这些通过 GrowthIntegration 中的具体调用处理
Ok(())
} }
} }
@@ -50,60 +57,37 @@ impl Default for UserProfileUpdater {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::sync::{Arc, Mutex};
#[tokio::test] #[test]
async fn test_update_industry() { fn test_collect_updates_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(())
};
let mut extraction = CombinedExtraction::default(); let mut extraction = CombinedExtraction::default();
extraction.profile_signals.industry = Some("healthcare".to_string()); extraction.profile_signals.industry = Some("healthcare".to_string());
let updater = UserProfileUpdater::new(); 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!(updates.len(), 1);
assert_eq!(locked.len(), 1); assert_eq!(updates[0].field, "industry");
assert_eq!(locked[0].1, "industry"); assert_eq!(updates[0].value, "healthcare");
assert_eq!(locked[0].2, "healthcare");
} }
#[tokio::test] #[test]
async fn test_update_no_signals() { fn test_collect_updates_no_signals() {
let update_fn =
|_: &str, _: &str, _: &str| -> zclaw_types::Result<()> { Ok(()) };
let extraction = CombinedExtraction::default(); let extraction = CombinedExtraction::default();
let updater = UserProfileUpdater::new(); let updater = UserProfileUpdater::new();
updater.update("user1", &extraction, update_fn).await.unwrap(); let updates = updater.collect_updates(&extraction);
// No panic = pass assert!(updates.is_empty());
} }
#[tokio::test] #[test]
async fn test_update_multiple_signals() { fn test_collect_updates_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(())
};
let mut extraction = CombinedExtraction::default(); let mut extraction = CombinedExtraction::default();
extraction.profile_signals.industry = Some("ecommerce".to_string()); extraction.profile_signals.industry = Some("ecommerce".to_string());
extraction.profile_signals.communication_style = Some("concise".to_string()); extraction.profile_signals.communication_style = Some("concise".to_string());
let updater = UserProfileUpdater::new(); 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!(updates.len(), 2);
assert_eq!(locked.len(), 2);
} }
} }

View File

@@ -84,6 +84,7 @@ mod tests {
body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(), body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(),
source_pattern: "报表生成".to_string(), source_pattern: "报表生成".to_string(),
confidence: 0.85, confidence: 0.85,
version: 1,
} }
} }

View File

@@ -15,6 +15,8 @@ pub struct SkillCandidate {
pub body_markdown: String, pub body_markdown: String,
pub source_pattern: String, pub source_pattern: String,
pub confidence: f32, pub confidence: f32,
/// 技能版本号,用于后续迭代追踪
pub version: u32,
} }
/// LLM 驱动的技能生成 prompt /// LLM 驱动的技能生成 prompt
@@ -59,8 +61,7 @@ impl SkillGenerator {
/// 解析 LLM 返回的 JSON 为 SkillCandidate /// 解析 LLM 返回的 JSON 为 SkillCandidate
pub fn parse_response(json_str: &str, pattern: &AggregatedPattern) -> Result<SkillCandidate> { pub fn parse_response(json_str: &str, pattern: &AggregatedPattern) -> Result<SkillCandidate> {
// 尝试提取 JSON 块LLM 可能包裹在 ```json ... ``` 中) let json_str = crate::json_utils::extract_json_block(json_str);
let json_str = extract_json_block(json_str);
let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
zclaw_types::ZclawError::ConfigError(format!("Invalid skill JSON: {}", e)) zclaw_types::ZclawError::ConfigError(format!("Invalid skill JSON: {}", e))
@@ -95,35 +96,11 @@ impl SkillGenerator {
body_markdown: raw["body_markdown"].as_str().unwrap_or("").to_string(), body_markdown: raw["body_markdown"].as_str().unwrap_or("").to_string(),
source_pattern: pattern.pain_pattern.clone(), source_pattern: pattern.pain_pattern.clone(),
confidence: raw["confidence"].as_f64().unwrap_or(0.5) as f32, confidence: raw["confidence"].as_f64().unwrap_or(0.5) as f32,
version: raw["version"].as_u64().unwrap_or(1) as u32,
}) })
} }
} }
/// 从 LLM 返回文本中提取 JSON 块
fn extract_json_block(text: &str) -> &str {
// 尝试匹配 ```json ... ```
if let Some(start) = text.find("```json") {
let json_start = start + 7; // skip ```json
if let Some(end) = text[json_start..].find("```") {
return text[json_start..json_start + end].trim();
}
}
// 尝试匹配 ``` ... ```
if let Some(start) = text.find("```") {
let json_start = start + 3;
if let Some(end) = text[json_start..].find("```") {
return text[json_start..json_start + end].trim();
}
}
// 尝试找 { ... } 块
if let Some(start) = text.find('{') {
if let Some(end) = text.rfind('}') {
return &text[start..=end];
}
}
text.trim()
}
impl Default for SkillGenerator { impl Default for SkillGenerator {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
@@ -194,12 +171,12 @@ mod tests {
#[test] #[test]
fn test_extract_json_block_with_markdown() { fn test_extract_json_block_with_markdown() {
let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone."; let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone.";
assert_eq!(extract_json_block(text), "{\"key\": \"value\"}"); assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}");
} }
#[test] #[test]
fn test_extract_json_block_bare() { fn test_extract_json_block_bare() {
let text = "{\"key\": \"value\"}"; let text = "{\"key\": \"value\"}";
assert_eq!(extract_json_block(text), "{\"key\": \"value\"}"); assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}");
} }
} }

View File

@@ -90,10 +90,10 @@ impl WorkflowComposer {
/// 解析 LLM 返回的 JSON 为 PipelineCandidate /// 解析 LLM 返回的 JSON 为 PipelineCandidate
pub fn parse_response( pub fn parse_response(
json_str: &str, json_str: &str,
pattern: &ToolChainPattern, _pattern: &ToolChainPattern,
source_sessions: Vec<String>, source_sessions: Vec<String>,
) -> Result<PipelineCandidate> { ) -> Result<PipelineCandidate> {
let json_str = extract_json_block(json_str); let json_str = crate::json_utils::extract_json_block(json_str);
let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
zclaw_types::ZclawError::ConfigError(format!("Invalid pipeline JSON: {}", e)) zclaw_types::ZclawError::ConfigError(format!("Invalid pipeline JSON: {}", e))
})?; })?;
@@ -118,28 +118,6 @@ impl WorkflowComposer {
} }
} }
/// 从 LLM 返回文本中提取 JSON 块
fn extract_json_block(text: &str) -> &str {
if let Some(start) = text.find("```json") {
let json_start = start + 7;
if let Some(end) = text[json_start..].find("```") {
return text[json_start..json_start + end].trim();
}
}
if let Some(start) = text.find("```") {
let json_start = start + 3;
if let Some(end) = text[json_start..].find("```") {
return text[json_start..json_start + end].trim();
}
}
if let Some(start) = text.find('{') {
if let Some(end) = text.rfind('}') {
return &text[start..=end];
}
}
text.trim()
}
impl Default for WorkflowComposer { impl Default for WorkflowComposer {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()

View File

@@ -324,25 +324,21 @@ impl GrowthIntegration {
// Update user profile from extraction signals (L1 enhancement) // Update user profile from extraction signals (L1 enhancement)
if let Some(profile_store) = &self.profile_store { if let Some(profile_store) = &self.profile_store {
let _store = profile_store.clone(); let updates = self
let user_id = agent_id.to_string();
if let Err(e) = self
.profile_updater .profile_updater
.update(&user_id, &combined_extraction, move |uid, field, val| { .collect_updates(&combined_extraction);
// Synchronous wrapper — the actual update_field is async, let user_id = agent_id.to_string();
// but we're already in an async context so we handle it via a future for update in updates {
// For now, log and let the store handle it if let Err(e) = profile_store
tracing::debug!( .update_field(&user_id, &update.field, &update.value)
"[GrowthIntegration] Profile update: {} {}={}",
uid,
field,
val
);
Ok(())
})
.await .await
{ {
tracing::warn!("[GrowthIntegration] Profile update failed: {}", e); tracing::warn!(
"[GrowthIntegration] Profile update failed for {}: {}",
update.field,
e
);
}
} }
} }

View File

@@ -68,13 +68,17 @@ impl AgentMiddleware for EvolutionMiddleware {
&self, &self,
ctx: &mut MiddlewareContext, ctx: &mut MiddlewareContext,
) -> Result<MiddlewareDecision> { ) -> Result<MiddlewareDecision> {
let pending = self.pending.read().await; let to_inject = {
let mut pending = self.pending.write().await;
if pending.is_empty() { if pending.is_empty() {
return Ok(MiddlewareDecision::Continue); return Ok(MiddlewareDecision::Continue);
} }
// 只取第一条(最近的)事件注入,避免信息过载
// drain 已注入的事件,防止重复注入
std::mem::take(&mut *pending)
};
// 只在第一条(最近的)事件上触发提示,避免信息过载 if let Some(evolution) = to_inject.into_iter().next() {
if let Some(evolution) = pending.first() {
let injection = format!( let injection = format!(
"\n\n<evolution-suggestion>\n\ "\n\n<evolution-suggestion>\n\
我注意到你经常做「{pattern}」相关的事情。\n\ 我注意到你经常做「{pattern}」相关的事情。\n\