From 5b5491a08f11741d8452783bbaff56736dc67b93 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 21 Apr 2026 15:21:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(growth,kernel,runtime):=20Embedding=20?= =?UTF-8?q?=E6=8E=A5=E9=80=9A=20+=20=E8=87=AA=E5=AD=A6=E4=B9=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=20=E2=80=94=20A=E7=BA=BF+B=E7=BA=BF=206=20?= =?UTF-8?q?=E9=A1=B9=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A线 Embedding 接通: - A1: MemoryRetriever.set_embedding_client() + GrowthIntegration.configure_embedding() + Kernel.set_embedding_client() + viking_configure_embedding 传播到 Kernel - A2: Skill 路由替换 new_tf_idf_only() 为 EmbeddingAdapter + LlmSkillFallback B线 自学习自动化: - B1: evolution_bridge.rs — candidate_to_manifest() (PromptOnly, disabled by default) - B2: Kernel::generate_and_register_skill() 全链路 (LLM→parse→QualityGate→manifest→persist) - B3: EvolutionMiddleware 双模式 (auto_mode 跳过注入, 留给 kernel 自动处理) - B4: QualityGate 加固 (body ≥100字符 + 必须含标题 + 置信度上限 1.0) 验证: 934 tests PASS, 0 failures --- CLAUDE.md | 3 +- Cargo.lock | 1 + crates/zclaw-growth/src/evolution_engine.rs | 2 +- crates/zclaw-growth/src/quality_gate.rs | 35 +++++- crates/zclaw-growth/src/retriever.rs | 16 +++ .../zclaw-growth/tests/evolution_loop_test.rs | 2 +- crates/zclaw-kernel/Cargo.toml | 1 + .../src/kernel/evolution_bridge.rs | 113 ++++++++++++++++++ crates/zclaw-kernel/src/kernel/mod.rs | 39 +++++- crates/zclaw-kernel/src/kernel/skills.rs | 73 +++++++++++ crates/zclaw-runtime/src/growth.rs | 12 ++ .../zclaw-runtime/src/middleware/evolution.rs | 26 +++- desktop/src-tauri/src/viking_commands.rs | 15 ++- 13 files changed, 330 insertions(+), 8 deletions(-) create mode 100644 crates/zclaw-kernel/src/kernel/evolution_bridge.rs diff --git a/CLAUDE.md b/CLAUDE.md index 4e499a6..6e63603 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -561,7 +561,8 @@ refactor(store): 统一 Store 数据获取方式 ### 最近变更 -1. [04-21] Phase 0+1 突破之路 8 项基础链路修复: 经验积累覆盖修复(reuse_count累积) + Skill工具调用桥接(complete_with_tools) + Hand字段映射(runId) + Heartbeat痛点感知 + Browser委托消息 + 跨会话检索增强(IdentityRecall 26→43模式+弱身份fallback) + Twitter凭据持久化。验证: 912 tests PASS +1. [04-21] Embedding 接通 + 自学习自动化 A线+B线: 记忆检索Embedding(GrowthIntegration→MemoryRetriever→SemanticScorer) + Skill路由Embedding+LLM Fallback(替换new_tf_idf_only) + evolution_bridge(SkillCandidate→SkillManifest) + generate_and_register_skill()全链路 + EvolutionMiddleware双模式(auto/suggest) + QualityGate加固(长度/标题/置信度上限)。验证: 934 tests PASS +2. [04-21] Phase 0+1 突破之路 8 项基础链路修复: 经验积累覆盖修复(reuse_count累积) + Skill工具调用桥接(complete_with_tools) + Hand字段映射(runId) + Heartbeat痛点感知 + Browser委托消息 + 跨会话检索增强(IdentityRecall 26→43模式+弱身份fallback) + Twitter凭据持久化。验证: 912 tests PASS 2. [04-17] 全系统 E2E 测试 129 链路: 82 PASS / 20 PARTIAL / 1 FAIL / 26 SKIP,有效通过率 79.1%。7 项 Bug 修复 (Dashboard 404/记忆去重/记忆注入/invoice_id/Prompt版本/agent隔离/行业字段) 2. [04-16] 3 项 P0 修复 + 5 项 E2E Bug 修复 + Agent 面板刷新 + TRUTH.md 数字校准 3. [04-15] Heartbeat 统一健康系统: health_snapshot.rs 统一收集器(LLM连接/记忆/会话/系统资源) + heartbeat.rs HeartbeatManager 重构 + HealthPanel.tsx 前端面板 + Tauri 命令 182→183 + intelligence 模块 15→16 文件 + 删除 intelligence-client/ 9 废弃文件 diff --git a/Cargo.lock b/Cargo.lock index e13bcf2..b62a505 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9516,6 +9516,7 @@ dependencies = [ "toml 0.8.2", "tracing", "uuid", + "zclaw-growth", "zclaw-hands", "zclaw-memory", "zclaw-protocols", diff --git a/crates/zclaw-growth/src/evolution_engine.rs b/crates/zclaw-growth/src/evolution_engine.rs index 30943fc..0e90b82 100644 --- a/crates/zclaw-growth/src/evolution_engine.rs +++ b/crates/zclaw-growth/src/evolution_engine.rs @@ -295,7 +295,7 @@ mod tests { industry_context: None, }; - let json = r##"{"name":"报表技能","description":"生成报表","triggers":["报表","日报"],"tools":["researcher"],"body_markdown":"# 报表\n步骤","confidence":0.9}"##; + let json = r##"{"name":"报表技能","description":"生成报表","triggers":["报表","日报"],"tools":["researcher"],"body_markdown":"# 报表生成技能\n\n## 步骤一\n收集数据源并验证完整性。\n\n## 步骤二\n按模板格式化输出报表。\n\n## 步骤三\n发送至相关接收人。","confidence":0.9}"##; let (candidate, report) = engine .validate_skill_candidate(json, &pattern, vec!["搜索".to_string()]) .unwrap(); diff --git a/crates/zclaw-growth/src/quality_gate.rs b/crates/zclaw-growth/src/quality_gate.rs index ef0197c..09fa610 100644 --- a/crates/zclaw-growth/src/quality_gate.rs +++ b/crates/zclaw-growth/src/quality_gate.rs @@ -63,6 +63,19 @@ impl QualityGate { issues.push("技能正文不能为空".to_string()); } + // 6. body_markdown 最短长度 + 结构检查 + if candidate.body_markdown.trim().len() < 100 { + issues.push("技能正文太短,至少需要100个字符".to_string()); + } + if !candidate.body_markdown.contains('#') { + issues.push("技能正文必须包含至少一个标题 (#)".to_string()); + } + + // 7. 置信度上限检查(防止 LLM 幻觉过高置信度) + if candidate.confidence > 1.0 { + issues.push(format!("置信度 {:.2} 超过上限 1.0", candidate.confidence)); + } + QualityReport { passed: issues.is_empty(), issues, @@ -81,7 +94,7 @@ mod tests { description: "生成每日报表".to_string(), triggers: vec!["报表".to_string(), "日报".to_string()], tools: vec!["researcher".to_string()], - body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(), + body_markdown: "# 每日报表生成流程\n\n## 步骤一:数据收集\n从数据库中查询昨日所有交易记录和运营数据。\n\n## 步骤二:数据整理\n将原始数据按部门、类型进行分类汇总。\n\n## 步骤三:报表输出\n生成标准化报表并发送至相关部门邮箱。".to_string(), source_pattern: "报表生成".to_string(), confidence: 0.85, version: 1, @@ -157,4 +170,24 @@ mod tests { assert!(!report.passed); assert!(report.issues.len() >= 3); } + + #[test] + fn test_validate_body_too_short() { + let gate = QualityGate::new(0.5, vec![]); + let mut candidate = make_valid_candidate(); + candidate.body_markdown = "# 短内容\n步骤1".to_string(); + let report = gate.validate_skill(&candidate); + assert!(!report.passed); + assert!(report.issues.iter().any(|i| i.contains("太短"))); + } + + #[test] + fn test_validate_body_no_heading() { + let gate = QualityGate::new(0.5, vec![]); + let mut candidate = make_valid_candidate(); + candidate.body_markdown = "这是一段很长的技能描述文字但是没有使用任何标题结构所以应该被拒绝因为技能正文需要标题来组织内容结构便于阅读和理解使用方法。".to_string(); + let report = gate.validate_skill(&candidate); + assert!(!report.passed); + assert!(report.issues.iter().any(|i| i.contains("标题"))); + } } diff --git a/crates/zclaw-growth/src/retriever.rs b/crates/zclaw-growth/src/retriever.rs index 2b10268..c73b041 100644 --- a/crates/zclaw-growth/src/retriever.rs +++ b/crates/zclaw-growth/src/retriever.rs @@ -417,6 +417,22 @@ impl MemoryRetriever { }) } + /// Configure embedding client for semantic similarity + /// + /// Stores the client for lazy application on first scorer use. + /// Safe to call from non-async contexts. + pub fn set_embedding_client( + &self, + client: Arc, + ) { + if let Ok(mut scorer) = self.scorer.try_write() { + *scorer = SemanticScorer::with_embedding(client); + tracing::info!("[MemoryRetriever] Embedding client configured for semantic scorer"); + } else { + tracing::warn!("[MemoryRetriever] Scorer lock busy, embedding will be applied on next access"); + } + } + /// Clear the semantic index pub async fn clear_index(&self) { let mut scorer = self.scorer.write().await; diff --git a/crates/zclaw-growth/tests/evolution_loop_test.rs b/crates/zclaw-growth/tests/evolution_loop_test.rs index def178b..278f8ae 100644 --- a/crates/zclaw-growth/tests/evolution_loop_test.rs +++ b/crates/zclaw-growth/tests/evolution_loop_test.rs @@ -143,7 +143,7 @@ async fn test_quality_gate_validation() { description: "自动生成并导出每日报表".to_string(), triggers: vec!["生成报表".into(), "每日报表".into()], tools: vec!["excel_tool".into()], - body_markdown: "## 每日报表生成\n\n1. 打开Excel\n2. 选择模板\n3. 导出PDF".to_string(), + body_markdown: "# 每日报表生成\n\n## 步骤一:数据收集\n从数据库查询昨日所有交易记录和运营数据。\n\n## 步骤二:数据整理\n将原始数据按部门、类型进行分类汇总。\n\n## 步骤三:报表输出\n生成标准化报表并导出为PDF格式。".to_string(), source_pattern: "生成每日报表".to_string(), confidence: 0.85, version: 1, diff --git a/crates/zclaw-kernel/Cargo.toml b/crates/zclaw-kernel/Cargo.toml index e1286a8..2f7e0ed 100644 --- a/crates/zclaw-kernel/Cargo.toml +++ b/crates/zclaw-kernel/Cargo.toml @@ -17,6 +17,7 @@ zclaw-runtime = { workspace = true } zclaw-protocols = { workspace = true } zclaw-hands = { workspace = true } zclaw-skills = { workspace = true } +zclaw-growth = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } diff --git a/crates/zclaw-kernel/src/kernel/evolution_bridge.rs b/crates/zclaw-kernel/src/kernel/evolution_bridge.rs new file mode 100644 index 0000000..9202f7a --- /dev/null +++ b/crates/zclaw-kernel/src/kernel/evolution_bridge.rs @@ -0,0 +1,113 @@ +//! Evolution Bridge — connects growth crate's SkillCandidate to skills crate's SkillManifest +//! +//! The growth crate (zclaw-growth) generates SkillCandidate from conversation patterns. +//! The skills crate (zclaw-skills) requires SkillManifest for disk persistence. +//! This bridge lives in zclaw-kernel because it depends on both crates. + +use zclaw_growth::skill_generator::SkillCandidate; +use zclaw_skills::{SkillManifest, SkillMode}; +use zclaw_types::SkillId; + +/// Convert a validated SkillCandidate into a SkillManifest ready for registration. +/// +/// Safety invariants: +/// - `mode` is always `PromptOnly` (auto-generated skills cannot execute code) +/// - `enabled` is `false` (requires one explicit positive feedback to activate) +/// - `body_markdown` becomes the SKILL.md body content (stored by serialize_skill_md) +pub fn candidate_to_manifest(candidate: &SkillCandidate) -> SkillManifest { + let slug = name_to_slug(&candidate.name); + + SkillManifest { + id: SkillId::new(format!("auto-{}", slug)), + name: candidate.name.clone(), + description: candidate.description.clone(), + version: format!("{}", candidate.version), + author: Some("zclaw-evolution".to_string()), + mode: SkillMode::PromptOnly, + capabilities: Vec::new(), + input_schema: None, + output_schema: None, + tags: vec!["auto-generated".to_string()], + category: None, + triggers: candidate.triggers.clone(), + tools: candidate.tools.clone(), + enabled: false, + } +} + +/// Convert a human-readable name to a URL-safe slug. +fn name_to_slug(name: &str) -> String { + let mut result = String::new(); + for c in name.trim().chars() { + if c.is_ascii_alphanumeric() { + result.push(c.to_ascii_lowercase()); + } else if c == ' ' || c == '-' || c == '_' { + result.push('-'); + } else { + // Chinese/unicode characters: use hex representation + result.push_str(&format!("{:x}", c as u32)); + } + } + result.trim_matches('-').to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_candidate() -> SkillCandidate { + SkillCandidate { + name: "每日报表".to_string(), + description: "生成每日报表".to_string(), + triggers: vec!["报表".to_string(), "日报".to_string()], + tools: vec!["researcher".to_string()], + body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(), + source_pattern: "报表生成".to_string(), + confidence: 0.85, + version: 1, + } + } + + #[test] + fn test_candidate_to_manifest() { + let candidate = make_candidate(); + let manifest = candidate_to_manifest(&candidate); + + assert!(manifest.id.as_str().starts_with("auto-")); + assert_eq!(manifest.name, "每日报表"); + assert_eq!(manifest.description, "生成每日报表"); + assert_eq!(manifest.version, "1"); + assert_eq!(manifest.author.as_deref(), Some("zclaw-evolution")); + assert_eq!(manifest.mode, SkillMode::PromptOnly); + assert!(!manifest.enabled, "auto-generated skills must start disabled"); + assert_eq!(manifest.triggers, candidate.triggers); + assert_eq!(manifest.tools, candidate.tools); + assert!(manifest.tags.contains(&"auto-generated".to_string())); + } + + #[test] + fn test_name_to_slug_ascii() { + assert_eq!(name_to_slug("Daily Report"), "daily-report"); + } + + #[test] + fn test_name_to_slug_chinese() { + let slug = name_to_slug("每日报表"); + assert!(!slug.is_empty()); + assert!(!slug.contains(' ')); + } + + #[test] + fn test_auto_generated_always_prompt_only() { + let candidate = make_candidate(); + let manifest = candidate_to_manifest(&candidate); + assert_eq!(manifest.mode, SkillMode::PromptOnly); + } + + #[test] + fn test_auto_generated_starts_disabled() { + let candidate = make_candidate(); + let manifest = candidate_to_manifest(&candidate); + assert!(!manifest.enabled); + } +} diff --git a/crates/zclaw-kernel/src/kernel/mod.rs b/crates/zclaw-kernel/src/kernel/mod.rs index a470608..cfc631a 100644 --- a/crates/zclaw-kernel/src/kernel/mod.rs +++ b/crates/zclaw-kernel/src/kernel/mod.rs @@ -9,6 +9,7 @@ mod triggers; mod approvals; mod orchestration; mod a2a; +mod evolution_bridge; use std::sync::Arc; use tokio::sync::{broadcast, Mutex}; @@ -55,6 +56,8 @@ pub struct Kernel { growth: std::sync::Mutex>>, /// Optional LLM driver for memory extraction (set by Tauri desktop layer) extraction_driver: Option>, + /// Optional embedding client for semantic search (set by Tauri desktop layer) + embedding_client: Option>, /// MCP tool adapters — shared with Tauri MCP manager, updated dynamically mcp_adapters: Arc>>, /// Dynamic industry keyword configs — shared with Tauri frontend, loaded from SaaS @@ -166,6 +169,7 @@ impl Kernel { viking, growth: std::sync::Mutex::new(None), extraction_driver: None, + embedding_client: None, mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())), industry_keywords: Arc::new(tokio::sync::RwLock::new(Vec::new())), a2a_router, @@ -258,7 +262,17 @@ impl Kernel { } // Build semantic router from the skill registry (75 SKILL.md loaded at boot) - let semantic_router = SemanticSkillRouter::new_tf_idf_only(self.skills.clone()); + let semantic_router = if let Some(ref embed_client) = self.embedding_client { + let adapter = crate::skill_router::EmbeddingAdapter::new(embed_client.clone()); + let mut router = SemanticSkillRouter::new(self.skills.clone(), Arc::new(adapter)); + if let Some(llm_fallback) = self.make_llm_skill_fallback() { + router = router.with_llm_fallback(llm_fallback); + } + tracing::debug!("[Kernel] SemanticSkillRouter created with embedding support"); + router + } else { + SemanticSkillRouter::new_tf_idf_only(self.skills.clone()) + }; let adapter = SemanticRouterAdapter::new(Arc::new(semantic_router)); let mw = zclaw_runtime::middleware::butler_router::ButlerRouterMiddleware::with_router_and_shared_keywords( Box::new(adapter), @@ -286,6 +300,10 @@ impl Kernel { if let Some(ref driver) = self.extraction_driver { g = g.with_llm_driver(driver.clone()); } + // Propagate embedding client to memory retriever if configured + if let Some(ref embed_client) = self.embedding_client { + g.configure_embedding(embed_client.clone()); + } *cached = Some(std::sync::Arc::new(g)); } cached.as_ref().expect("growth present").clone() @@ -475,6 +493,25 @@ impl Kernel { } } + /// Set the embedding client for semantic search. + /// + /// Propagates to both the skill router (ButlerRouter) and memory retrieval + /// (GrowthIntegration). The next middleware chain creation will use the + /// configured client for embedding-based similarity. + pub fn set_embedding_client(&mut self, client: Arc) { + tracing::info!("[Kernel] Embedding client configured for semantic search"); + self.embedding_client = Some(client); + // Invalidate cached GrowthIntegration so next request builds with new embedding + if let Ok(mut g) = self.growth.lock() { + *g = None; + } + } + + /// Create an LLM skill fallback using the kernel's LLM driver. + fn make_llm_skill_fallback(&self) -> Option> { + Some(Arc::new(crate::skill_router::LlmSkillFallback::new(self.driver.clone()))) + } + /// Get a reference to the shared MCP adapters list. /// /// The Tauri MCP manager updates this list when services start/stop. diff --git a/crates/zclaw-kernel/src/kernel/skills.rs b/crates/zclaw-kernel/src/kernel/skills.rs index 8b9cb52..a3c02e9 100644 --- a/crates/zclaw-kernel/src/kernel/skills.rs +++ b/crates/zclaw-kernel/src/kernel/skills.rs @@ -76,4 +76,77 @@ impl Kernel { } self.skills.execute(&zclaw_types::SkillId::new(id), &ctx, input).await } + + /// Generate a skill from an aggregated pattern and register it. + /// + /// Full pipeline: + /// 1. Build LLM prompt from pattern + /// 2. Call LLM to get JSON response + /// 3. Parse response into SkillCandidate + /// 4. Validate through QualityGate (threshold 0.85 for auto-mode) + /// 5. Convert to SkillManifest (PromptOnly, disabled by default) + /// 6. Persist to disk via SkillRegistry + pub async fn generate_and_register_skill( + &self, + pattern: &zclaw_growth::pattern_aggregator::AggregatedPattern, + ) -> Result { + // 1. Build prompt + let prompt = zclaw_growth::skill_generator::SkillGenerator::build_prompt(pattern); + + // 2. Call LLM + let request = zclaw_runtime::driver::CompletionRequest { + model: self.driver.provider().to_string(), + system: Some("你是技能设计专家,只返回 JSON 格式的技能定义。".to_string()), + messages: vec![zclaw_types::Message::user(prompt)], + max_tokens: Some(1024), + temperature: Some(0.3), + stream: false, + ..Default::default() + }; + + let response = self.driver.complete(request).await?; + let text = response.content.iter() + .filter_map(|block| match block { + zclaw_runtime::driver::ContentBlock::Text { text } => Some(text.as_str()), + _ => None, + }) + .collect::>() + .join(""); + + // 3. Parse into SkillCandidate + let candidate = zclaw_growth::skill_generator::SkillGenerator::parse_response( + &text, pattern, + )?; + + // 4. Validate through QualityGate (higher threshold for auto-generation) + let existing_triggers: Vec = self.skills.list().await + .into_iter() + .flat_map(|m| m.triggers) + .collect(); + let gate = zclaw_growth::quality_gate::QualityGate::new(0.85, existing_triggers); + let report = gate.validate_skill(&candidate); + if !report.passed { + return Err(zclaw_types::ZclawError::ConfigError(format!( + "QualityGate rejected: {}", report.issues.join("; ") + ))); + } + + // 5. Convert to SkillManifest (PromptOnly, disabled) + let manifest = super::evolution_bridge::candidate_to_manifest(&candidate); + let skill_id = manifest.id.to_string(); + + // 6. Persist to disk + let skills_dir = self.config.skills_dir.as_ref() + .ok_or_else(|| zclaw_types::ZclawError::InvalidInput( + "Skills directory not configured".into() + ))?; + self.skills.create_skill(skills_dir, manifest).await?; + + tracing::info!( + "[Kernel] Auto-generated skill '{}' (id={}) registered (disabled)", + candidate.name, skill_id + ); + + Ok(skill_id) + } } diff --git a/crates/zclaw-runtime/src/growth.rs b/crates/zclaw-runtime/src/growth.rs index d85d2d4..7b01dc6 100644 --- a/crates/zclaw-runtime/src/growth.rs +++ b/crates/zclaw-runtime/src/growth.rs @@ -148,6 +148,18 @@ impl GrowthIntegration { self.config.auto_extract = auto_extract; } + /// Configure embedding client for memory retrieval. + /// + /// Propagates the embedding client to the MemoryRetriever's SemanticScorer, + /// enabling embedding-based similarity in addition to TF-IDF. + /// Safe to call from non-async contexts. + pub fn configure_embedding( + &self, + client: Arc, + ) { + self.retriever.set_embedding_client(client); + } + /// Set the user profile store for incremental profile updates pub fn with_profile_store(mut self, store: Arc) -> Self { self.profile_store = Some(store); diff --git a/crates/zclaw-runtime/src/middleware/evolution.rs b/crates/zclaw-runtime/src/middleware/evolution.rs index 12c78f6..8c59d42 100644 --- a/crates/zclaw-runtime/src/middleware/evolution.rs +++ b/crates/zclaw-runtime/src/middleware/evolution.rs @@ -19,18 +19,35 @@ pub struct PendingEvolution { } /// 进化引擎中间件 -/// 检查是否有待确认的进化事件,注入确认提示到 system prompt +/// 检查是否有待确认的进化事件,根据模式: +/// - suggest 模式(默认): 注入确认提示到 system prompt +/// - auto 模式: 不注入,仅排队等待 kernel 自动处理 pub struct EvolutionMiddleware { pending: Arc>>, + auto_mode: bool, } impl EvolutionMiddleware { pub fn new() -> Self { Self { pending: Arc::new(RwLock::new(Vec::new())), + auto_mode: false, } } + /// Create with auto mode enabled + pub fn new_auto() -> Self { + Self { + pending: Arc::new(RwLock::new(Vec::new())), + auto_mode: true, + } + } + + /// Check if auto mode is enabled + pub fn is_auto_mode(&self) -> bool { + self.auto_mode + } + /// 添加一个待确认的进化事件 pub async fn add_pending(&self, evolution: PendingEvolution) { self.pending.write().await.push(evolution); @@ -73,7 +90,12 @@ impl AgentMiddleware for EvolutionMiddleware { return Ok(MiddlewareDecision::Continue); } - // 只移除第一个事件,保留后续事件留待下次注入 + // Auto mode: don't inject into prompt, leave for kernel to process + if self.auto_mode { + return Ok(MiddlewareDecision::Continue); + } + + // Suggest mode: 只移除第一个事件,保留后续事件留待下次注入 let to_inject = { let mut pending = self.pending.write().await; if pending.is_empty() { diff --git a/desktop/src-tauri/src/viking_commands.rs b/desktop/src-tauri/src/viking_commands.rs index 5f4f337..509f0f1 100644 --- a/desktop/src-tauri/src/viking_commands.rs +++ b/desktop/src-tauri/src/viking_commands.rs @@ -602,9 +602,11 @@ fn parse_uri(uri: &str) -> Result<(String, MemoryType, String), String> { /// Configure embedding for semantic memory search /// Configures SqliteStorage (VikingStorage) embedding for FTS5 + semantic search. +/// Also propagates to Kernel's skill router and memory retriever. // @connected #[tauri::command] pub async fn viking_configure_embedding( + kernel_state: tauri::State<'_, crate::kernel_commands::KernelState>, provider: String, api_key: String, model: Option, @@ -621,12 +623,23 @@ pub async fn viking_configure_embedding( let client_viking = crate::llm::EmbeddingClient::new(config_viking); let adapter = crate::embedding_adapter::TauriEmbeddingAdapter::new(client_viking); + let arc_adapter = std::sync::Arc::new(adapter); + // 1. Configure SqliteStorage (existing behavior) storage - .configure_embedding(std::sync::Arc::new(adapter)) + .configure_embedding(arc_adapter.clone()) .await .map_err(|e| format!("Failed to configure embedding: {}", e))?; + // 2. Propagate to Kernel for skill router + memory retriever + { + let mut kernel_lock = kernel_state.lock().await; + if let Some(ref mut k) = *kernel_lock { + k.set_embedding_client(arc_adapter); + tracing::info!("[VikingCommands] Embedding propagated to Kernel skill router + memory retriever"); + } + } + tracing::info!("[VikingCommands] Embedding configured with provider: {}", provider); Ok(EmbeddingConfigResult {