From e9e7ffd609d428cdaa61e6537a84d4131bf84bff Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 23 Apr 2026 17:52:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(intelligence):=20=E6=96=B0=E5=A2=9E=20expe?= =?UTF-8?q?rience=5Ffind=5Frelevant=20Tauri=20=E5=91=BD=E4=BB=A4=20+=20Exp?= =?UTF-8?q?erienceBrief?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ExperienceBrief 结构(痛点模式+方案摘要+复用次数) - OnceLock 单例 + init_experience_extractor() 启动初始化 - experience_find_relevant 命令:按 agent_id + query 检索相关经验 - 注册到 invoke_handler + setup 阶段优雅降级初始化 - 新增序列化测试(10 tests PASS) --- .../src-tauri/src/intelligence/experience.rs | 58 +++++++++++++++++++ desktop/src-tauri/src/lib.rs | 8 +++ 2 files changed, 66 insertions(+) diff --git a/desktop/src-tauri/src/intelligence/experience.rs b/desktop/src-tauri/src/intelligence/experience.rs index 842b89b..1393643 100644 --- a/desktop/src-tauri/src/intelligence/experience.rs +++ b/desktop/src-tauri/src/intelligence/experience.rs @@ -16,6 +16,21 @@ use zclaw_types::Result; use super::pain_aggregator::PainPoint; use super::solution_generator::Proposal; +/// Brief summary of a stored experience, for suggestion context enrichment. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExperienceBrief { + pub pain_pattern: String, + pub solution_summary: String, + pub reuse_count: u32, +} + +static EXPERIENCE_EXTRACTOR: std::sync::OnceLock> = std::sync::OnceLock::new(); + +/// Get the global ExperienceExtractor singleton (if initialized). +pub(crate) fn get_experience_extractor() -> Option> { + EXPERIENCE_EXTRACTOR.get().cloned() +} + // --------------------------------------------------------------------------- // Shared completion status // --------------------------------------------------------------------------- @@ -263,6 +278,36 @@ fn xml_escape(s: &str) -> String { .replace('>', ">") } +/// Initialize the global ExperienceExtractor singleton. +/// Called once during app startup, after viking storage is ready. +pub async fn init_experience_extractor() -> Result<()> { + let sqlite_storage = crate::viking_commands::get_storage().await + .map_err(|e| zclaw_types::ZclawError::StorageError(e))?; + let viking = std::sync::Arc::new(zclaw_growth::VikingAdapter::new(sqlite_storage)); + let store = std::sync::Arc::new(ExperienceStore::new(viking)); + let extractor = std::sync::Arc::new(ExperienceExtractor::new(store)); + EXPERIENCE_EXTRACTOR.set(extractor) + .map_err(|_| zclaw_types::ZclawError::StorageError("ExperienceExtractor already initialized".into()))?; + Ok(()) +} + +/// Find experiences relevant to the current conversation for suggestion enrichment. +#[tauri::command] +pub async fn experience_find_relevant( + agent_id: String, + query: String, +) -> std::result::Result, String> { + let extractor = get_experience_extractor() + .ok_or("ExperienceExtractor not initialized".to_string())?; + let experiences = extractor.find_relevant_experiences(&agent_id, &query).await; + Ok(experiences.into_iter().take(3).map(|e| ExperienceBrief { + pain_pattern: e.pain_pattern, + solution_summary: e.solution_steps.join(";") + .chars().take(100).collect(), + reuse_count: e.reuse_count, + }).collect()) +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -407,4 +452,17 @@ mod tests { assert_eq!(truncate("hello", 10), "hello"); assert_eq!(truncate("这是一个很长的字符串用于测试截断", 10).chars().count(), 11); // 10 + … } + + #[test] + fn test_experience_brief_serialization() { + let brief = super::ExperienceBrief { + pain_pattern: "报表生成慢".to_string(), + solution_summary: "使用 researcher 技能自动收集".to_string(), + reuse_count: 3, + }; + let json = serde_json::to_string(&brief).unwrap(); + let parsed: super::ExperienceBrief = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.pain_pattern, "报表生成慢"); + assert_eq!(parsed.reuse_count, 3); + } } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index be9bbd0..cdd736f 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -212,6 +212,12 @@ pub fn run() { if let Err(e) = rt.block_on(intelligence::pain_aggregator::init_pain_storage(pool)) { tracing::error!("[PainStorage] Init failed: {}, pain points will not persist", e); } + + // Initialize experience extractor for suggestion enrichment. + // Graceful degradation: failure does not block app startup. + if let Err(e) = rt.block_on(intelligence::experience::init_experience_extractor()) { + tracing::warn!("[ExperienceExtractor] Init failed: {}, suggestion context will be empty", e); + } } Ok(()) @@ -435,6 +441,8 @@ pub fn run() { intelligence::pain_aggregator::butler_update_proposal_status, // Industry config loader viking_commands::viking_load_industry_keywords, + // Experience finder for suggestion enrichment + intelligence::experience::experience_find_relevant, ]) .run(tauri::generate_context!()) .expect("error while running tauri application");