[appimage] Phase 2.0: Proaktive Intelligenz
All checks were successful
Build AppImage / build (push) Successful in 8m19s

- MySQL Pool als Managed State (MysqlPoolState in lib.rs)
- Keyword-Extraktion aus User-Nachrichten (Stoppwort-Filter DE/EN)
- Proaktive KB-Abfrage bei SessionStart (proactive_session_hints)
- Auto-Fehler-Pattern: error_tracker Tabelle, bei 3+ Occurrences
  automatisch KB-Eintrag in Kategorie 'fehler' erstellen
- 6 neue Tauri-Commands für KB-Hints und Error-Tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-20 14:19:29 +02:00
parent d315f421ec
commit 2de88a2a22
6 changed files with 476 additions and 11 deletions

View file

@ -1346,11 +1346,22 @@ END;
## In Arbeit / Geplant ## In Arbeit / Geplant
### Phase 2.0: Proaktive Intelligenz (geplant) ### Phase 2.0: Proaktive Intelligenz ✅ ERLEDIGT (20.04.2026)
- [ ] MySQL Pool als Managed State (Effizienz-Fix für knowledge.rs)
- [ ] Proaktive KB-Abfrage bei SessionStart | Feature | Status | Datei(en) |
- [ ] Themen-Erkennung aus User-Nachrichten für KB-Suche |---------|--------|-----------|
- [ ] Auto-Fehler-Pattern-Speicherung (3x gleicher Fehler → Pattern) | MySQL Pool als Managed State | ✅ | `knowledge.rs`, `lib.rs` |
| Proaktive KB-Abfrage bei SessionStart | ✅ | `knowledge.rs`, `claude.rs`, `events.ts` |
| Themen-Erkennung (Keyword-Extraktion) | ✅ | `knowledge.rs` (`extract_keywords()`, Stoppwort-Filter) |
| Auto-Fehler-Pattern (3x → KB-Eintrag) | ✅ | `db.rs` (`error_tracker` Tabelle), `events.ts`, `knowledge.rs` |
#### Details
- **MySQL Pool**: `MysqlPoolState` als Tauri Managed State, einmal beim App-Start erstellt
- **Proaktive Hints**: `proactive_session_hints()` lädt bei Session-Start relevante KB-Einträge basierend auf Projekt-Kontext
- **Keyword-Extraktion**: Deutsche + englische Stoppwörter gefiltert, max 6 Keywords, dedupliziert
- **Error-Tracking**: `error_tracker` SQLite-Tabelle mit Hash-basiertem Grouping. Bei 3+ Occurrences wird automatisch ein `fehler`-Eintrag in der claude-db (MySQL) erstellt
- **Neue Tauri-Commands**: `extract_message_keywords`, `get_session_hints`, `auto_save_error_pattern`, `track_error`, `set_error_kb_pattern`, `get_error_stats`
### Phase 2.1: Desktop-Zugriff erweitern (geplant) ### Phase 2.1: Desktop-Zugriff erweitern (geplant)
- [ ] Guard-Rails in Claude-Bridge einbauen - [ ] Guard-Rails in Claude-Bridge einbauen

View file

@ -665,7 +665,38 @@ pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, St
} }
} }
if let Some(ref ctx) = context { // Phase 2.0: Proaktive KB-Hints bei Session-Start laden
// Projekt-Name aus Sticky Context extrahieren falls vorhanden
let project_name = if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
let db = db_state.lock().unwrap();
db.get_setting("current_project_name").ok().flatten()
} else {
None
};
let proactive_hints = match knowledge::proactive_session_hints(project_name.as_deref()).await {
Ok(hints) if !hints.is_empty() => {
println!("📋 Proaktive Session-Hints: {} Bytes", hints.len());
Some(hints)
}
Ok(_) => None,
Err(e) => {
println!("⚠️ Proaktive Hints Fehler (ignoriert): {}", e);
None
}
};
// Context + proaktive Hints kombinieren
let mut full_context = context.clone();
if let Some(hints) = proactive_hints {
let ctx = full_context.get_or_insert_with(String::new);
if !ctx.is_empty() {
ctx.push_str("\n\n");
}
ctx.push_str(&hints);
}
if let Some(ref ctx) = full_context {
info.loaded = true; info.loaded = true;
info.estimated_tokens = ctx.len() / 4; info.estimated_tokens = ctx.len() / 4;
@ -685,7 +716,7 @@ pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, St
// Context an Bridge senden // Context an Bridge senden
let _ = send_to_bridge(&app, "set-context", ctx); let _ = send_to_bridge(&app, "set-context", ctx);
println!("✅ Sticky Context geladen: {} Einträge, ~{} Token", info.entries, info.estimated_tokens); println!("✅ Sticky Context geladen: {} Einträge, ~{} Token (inkl. proaktive Hints)", info.entries, info.estimated_tokens);
} else { } else {
println!(" Kein Sticky Context konfiguriert"); println!(" Kein Sticky Context konfiguriert");
} }

View file

@ -204,6 +204,19 @@ impl Database {
DELETE FROM monitor_events DELETE FROM monitor_events
WHERE timestamp < datetime('now', '-7 days'); WHERE timestamp < datetime('now', '-7 days');
END; END;
-- Phase 2.0: Fehler-Tracking für Auto-Pattern-Erkennung
CREATE TABLE IF NOT EXISTS error_tracker (
error_hash TEXT PRIMARY KEY,
error_message TEXT NOT NULL,
tool TEXT NOT NULL,
occurrence_count INTEGER DEFAULT 1,
first_seen TEXT NOT NULL,
last_seen TEXT NOT NULL,
kb_pattern_id INTEGER,
UNIQUE(error_hash)
);
CREATE INDEX IF NOT EXISTS idx_error_tracker_count ON error_tracker(occurrence_count DESC);
", ",
)?; )?;
Ok(()) Ok(())
@ -839,6 +852,66 @@ impl Database {
Ok(counts) Ok(counts)
} }
// ============ Phase 2.0: Fehler-Tracking ============
/// Fehler-Occurrence zählen und zurückgeben
/// Gibt (neuer_count, error_message, tool, kb_pattern_id) zurück
pub fn track_error(&self, error_hash: &str, error_message: &str, tool: &str) -> SqlResult<(i32, Option<i64>)> {
let now = chrono::Local::now().to_rfc3339();
// Versuche zu aktualisieren
let updated = self.conn.execute(
"UPDATE error_tracker SET
occurrence_count = occurrence_count + 1,
last_seen = ?1,
error_message = ?2
WHERE error_hash = ?3",
params![now, error_message, error_hash],
)?;
if updated == 0 {
// Neuer Eintrag
self.conn.execute(
"INSERT INTO error_tracker (error_hash, error_message, tool, occurrence_count, first_seen, last_seen)
VALUES (?1, ?2, ?3, 1, ?4, ?4)",
params![error_hash, error_message, tool, now],
)?;
return Ok((1, None));
}
// Aktuellen Count und kb_pattern_id holen
let result: (i32, Option<i64>) = self.conn.query_row(
"SELECT occurrence_count, kb_pattern_id FROM error_tracker WHERE error_hash = ?1",
params![error_hash],
|row| Ok((row.get(0)?, row.get(1)?)),
)?;
Ok(result)
}
/// KB-Pattern-ID für einen Fehler speichern (nachdem Pattern in KB erstellt wurde)
pub fn set_error_kb_pattern(&self, error_hash: &str, kb_pattern_id: i64) -> SqlResult<()> {
self.conn.execute(
"UPDATE error_tracker SET kb_pattern_id = ?1 WHERE error_hash = ?2",
params![kb_pattern_id, error_hash],
)?;
Ok(())
}
/// Fehler-Statistiken laden (Top N häufigste Fehler)
pub fn get_error_stats(&self, limit: usize) -> SqlResult<Vec<(String, String, String, i32, Option<i64>)>> {
let mut stmt = self.conn.prepare(
"SELECT error_hash, error_message, tool, occurrence_count, kb_pattern_id
FROM error_tracker
ORDER BY occurrence_count DESC
LIMIT ?1"
)?;
let stats = stmt.query_map(params![limit as i64], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?))
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(stats)
}
// ============ Statistiken ============ // ============ Statistiken ============
/// DB-Statistiken /// DB-Statistiken
@ -1043,3 +1116,42 @@ pub async fn get_monitor_stats(app: AppHandle) -> Result<Vec<(String, usize)>, S
let db = state.lock().unwrap(); let db = state.lock().unwrap();
db.count_monitor_events_by_type().map_err(|e| e.to_string()) db.count_monitor_events_by_type().map_err(|e| e.to_string())
} }
// ============ Phase 2.0: Fehler-Tracking Commands ============
/// Fehler tracken — gibt (count, kb_pattern_id) zurück
#[tauri::command]
pub async fn track_error(
app: AppHandle,
error_hash: String,
error_message: String,
tool: String,
) -> Result<(i32, Option<i64>), String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.track_error(&error_hash, &error_message, &tool).map_err(|e| e.to_string())
}
/// KB-Pattern-ID für Fehler setzen
#[tauri::command]
pub async fn set_error_kb_pattern(
app: AppHandle,
error_hash: String,
kb_pattern_id: i64,
) -> Result<(), String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.set_error_kb_pattern(&error_hash, kb_pattern_id).map_err(|e| e.to_string())
}
/// Fehler-Statistiken laden
#[tauri::command]
pub async fn get_error_stats(
app: AppHandle,
limit: Option<usize>,
) -> Result<Vec<(String, String, String, i32, Option<i64>)>, String> {
let limit = limit.unwrap_or(20);
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.get_error_stats(limit).map_err(|e| e.to_string())
}

View file

@ -1,9 +1,12 @@
// Claude Desktop — Wissensbasis (claude-db) // Claude Desktop — Wissensbasis (claude-db)
// Direkte MySQL-Anbindung zur zentralen Wissensdatenbank // Direkte MySQL-Anbindung zur zentralen Wissensdatenbank
// Phase 2.0: MySQL Pool als Managed State + Themen-Erkennung
use mysql_async::{Pool, prelude::*}; use mysql_async::{Pool, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use std::sync::Arc;
use tauri::{AppHandle, Manager};
/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults /// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults
const MYSQL_PORT: u16 = 3306; const MYSQL_PORT: u16 = 3306;
@ -13,6 +16,86 @@ fn mysql_user() -> String { std::env::var("CLAUDE_MYSQL_USER").unwrap_or_else(|_
fn mysql_pass() -> String { std::env::var("CLAUDE_MYSQL_PASS").unwrap_or_else(|_| "8715".to_string()) } fn mysql_pass() -> String { std::env::var("CLAUDE_MYSQL_PASS").unwrap_or_else(|_| "8715".to_string()) }
fn mysql_db() -> String { std::env::var("CLAUDE_MYSQL_DB").unwrap_or_else(|_| "claude".to_string()) } fn mysql_db() -> String { std::env::var("CLAUDE_MYSQL_DB").unwrap_or_else(|_| "claude".to_string()) }
/// Managed MySQL Pool — wird einmal beim App-Start erstellt
/// Alle Knowledge-Funktionen nutzen diesen Pool statt jedes Mal einen neuen zu erstellen
pub type MysqlPoolState = Arc<Option<Pool>>;
/// Erstellt den globalen MySQL Pool (einmal beim App-Start aufrufen)
pub fn create_managed_pool() -> MysqlPoolState {
let url = format!(
"mysql://{}:{}@{}:{}/{}",
mysql_user(), mysql_pass(), mysql_host(), MYSQL_PORT, mysql_db()
);
match Pool::new(url.as_str()) {
pool => {
println!("🗄️ MySQL Pool erstellt ({}:{})", mysql_host(), MYSQL_PORT);
Arc::new(Some(pool))
}
}
}
/// Pool aus AppHandle holen — Fallback auf neuen Pool wenn State nicht verfügbar
/// TODO: Bestehende Commands schrittweise auf get_pool(app) migrieren
#[allow(dead_code)]
fn get_pool(app: Option<&AppHandle>) -> Pool {
if let Some(app) = app {
if let Some(pool_state) = app.try_state::<MysqlPoolState>() {
if let Some(pool) = pool_state.as_ref() {
return pool.clone();
}
}
}
// Fallback: neuen Pool erstellen (für Aufrufe ohne AppHandle, z.B. search_knowledge_internal)
create_pool()
}
/// Deutsche Stoppwörter die bei der Themen-Erkennung gefiltert werden
const STOP_WORDS: &[&str] = &[
"der", "die", "das", "den", "dem", "des", "ein", "eine", "einer", "einem", "einen",
"und", "oder", "aber", "doch", "noch", "auch", "nur", "schon", "mal", "dann",
"ist", "sind", "war", "hat", "haben", "wird", "werden", "kann", "können", "soll",
"muss", "müssen", "darf", "will", "wollen", "möchte", "würde", "sollte",
"ich", "du", "er", "sie", "es", "wir", "ihr", "mein", "dein", "sein",
"mit", "für", "auf", "von", "aus", "bei", "nach", "über", "unter", "vor",
"wie", "was", "wer", "wo", "wann", "warum", "wieso", "weshalb",
"nicht", "kein", "keine", "keinen", "keinem",
"this", "that", "the", "and", "for", "with", "from", "into",
"bitte", "danke", "okay", "alles", "nächste", "mach", "zeig", "gib",
"mir", "dir", "uns", "hier", "dort", "jetzt", "gerade", "einfach",
"phase", "feature", "erstelle", "implementiere", "baue",
];
/// Extrahiert relevante Keywords aus einer User-Nachricht
/// Filtert Stoppwörter und kurze Wörter raus, gibt die besten Suchbegriffe zurück
pub fn extract_keywords(message: &str) -> Vec<String> {
let words: Vec<String> = message
.to_lowercase()
// Satzzeichen entfernen
.replace(|c: char| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.', " ")
.split_whitespace()
.filter(|w| {
w.len() >= 3
&& !STOP_WORDS.contains(&w.as_ref())
// Zahlen alleine sind selten gute Suchbegriffe
&& !w.chars().all(|c| c.is_numeric())
})
.map(|w| w.to_string())
.collect();
// Deduplizieren und max 6 Keywords behalten
let mut unique: Vec<String> = Vec::new();
for w in words {
if !unique.contains(&w) {
unique.push(w);
}
if unique.len() >= 6 {
break;
}
}
unique
}
/// Wissenseintrag aus der knowledge-Tabelle /// Wissenseintrag aus der knowledge-Tabelle
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeEntry { pub struct KnowledgeEntry {
@ -69,7 +152,23 @@ fn create_pool() -> Pool {
/// KB-Hints für eine Nachricht laden — fehlertolerant, gibt leeren String bei DB-Problemen /// KB-Hints für eine Nachricht laden — fehlertolerant, gibt leeren String bei DB-Problemen
/// Wird von claude.rs aufgerufen bevor die Nachricht an die Bridge geht /// Wird von claude.rs aufgerufen bevor die Nachricht an die Bridge geht
/// Phase 2.0: Nutzt jetzt Keyword-Extraktion statt rohe Nachricht als Query
pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String, String> { pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String, String> {
// Phase 2.0: Keywords aus der Nachricht extrahieren für bessere Suche
let keywords = extract_keywords(query);
let search_query = if keywords.is_empty() {
// Fallback: erste 100 Zeichen der Nachricht
query[..query.len().min(100)].to_string()
} else {
keywords.join(" ")
};
search_knowledge_by_query(&search_query, limit).await
}
/// Sucht in der KB mit einem vorbereiteten Query-String
/// Zentrale Suchfunktion die von search_knowledge_internal und proactive_session_hints genutzt wird
pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result<String, String> {
let pool = create_pool(); let pool = create_pool();
// Verbindung mit Timeout — DB nicht erreichbar soll nicht blockieren // Verbindung mit Timeout — DB nicht erreichbar soll nicht blockieren
@ -102,14 +201,14 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
ORDER BY priority DESC, relevance DESC ORDER BY priority DESC, relevance DESC
LIMIT ?"#, LIMIT ?"#,
(query, query, limit), (search_query, search_query, limit),
).await.map_err(|e| e.to_string())?; ).await.map_err(|e| e.to_string())?;
drop(conn); drop(conn);
let _ = pool.disconnect().await; let _ = pool.disconnect().await;
if results.is_empty() { if results.is_empty() {
println!("🔍 KB-Hints für '{}': keine Treffer", &query[..query.len().min(40)]); println!("🔍 KB-Hints für '{}': keine Treffer", &search_query[..search_query.len().min(40)]);
return Ok(String::new()); return Ok(String::new());
} }
@ -132,11 +231,102 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
let block = hints.join("\n"); let block = hints.join("\n");
println!("🔍 KB-Hints für '{}': {} Treffer, {} Bytes", println!("🔍 KB-Hints für '{}': {} Treffer, {} Bytes",
&query[..query.len().min(40)], results.len(), block.len()); &search_query[..search_query.len().min(40)], results.len(), block.len());
Ok(block) Ok(block)
} }
/// Phase 2.0: Proaktive KB-Abfrage bei SessionStart
/// Lädt relevante Einträge basierend auf Projekt-Kontext und letzter Aktivität
pub async fn proactive_session_hints(project_name: Option<&str>) -> Result<String, String> {
let mut search_terms = Vec::new();
// Projekt-bezogene Begriffe
if let Some(name) = project_name {
search_terms.push(name.to_string());
// Projekt-spezifische Zusatzbegriffe
let lower = name.to_lowercase();
if lower.contains("dolibarr") { search_terms.push("dolibarr".to_string()); }
if lower.contains("claude") { search_terms.push("claude desktop tauri".to_string()); }
if lower.contains("bericht") { search_terms.push("bericht modul".to_string()); }
if lower.contains("kunden") { search_terms.push("kundenkarte schaltplan".to_string()); }
}
// Allgemeine Begriffe für Session-Start (aktive Fehler, wichtige Patterns)
search_terms.push("fehler workaround aktiv".to_string());
let query = search_terms.join(" ");
println!("📋 Proaktive KB-Abfrage: '{}'", &query[..query.len().min(60)]);
search_knowledge_by_query(&query, 5).await
}
/// Phase 2.0: Auto-Fehler-Pattern in KB speichern
/// Wird aufgerufen wenn ein Fehler 3x aufgetreten ist
pub async fn save_error_pattern_to_kb(
error_hash: &str,
error_message: &str,
tool: &str,
occurrence_count: i32,
) -> Result<i64, String> {
let pool = create_pool();
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
// Prüfen ob für diesen Hash schon ein KB-Eintrag existiert
let existing: Option<i64> = conn.exec_first(
r#"SELECT id FROM knowledge
WHERE category = 'fehler'
AND tags LIKE CONCAT('%', ?, '%')
AND status = 'active'
LIMIT 1"#,
(error_hash,),
).await.map_err(|e| e.to_string())?;
if let Some(id) = existing {
// Schon vorhanden — nur Occurrence aktualisieren
conn.exec_drop(
r#"UPDATE knowledge SET
content = CONCAT(content, '\n\n---\nWeiteres Auftreten: ', NOW(), ' (', ?, 'x gesamt)'),
updated_at = NOW()
WHERE id = ?"#,
(occurrence_count, id),
).await.map_err(|e| e.to_string())?;
drop(conn);
let _ = pool.disconnect().await;
println!("📝 Fehler-Pattern #{} aktualisiert ({}x)", id, occurrence_count);
return Ok(id);
}
// Neuen KB-Eintrag erstellen
let title = format!("Auto-Pattern: {} Fehler in {}", &error_message[..error_message.len().min(60)], tool);
let content = format!(
"## Automatisch erkanntes Fehler-Pattern\n\n\
**Tool:** {}\n\
**Häufigkeit:** {}x aufgetreten\n\
**Fehlermeldung:**\n```\n{}\n```\n\n\
**Hash:** `{}`\n\n\
> Dieses Pattern wurde automatisch erstellt nachdem der Fehler {}x aufgetreten ist.\n\
> Bitte Lösung/Workaround ergänzen.",
tool, occurrence_count, &error_message[..error_message.len().min(500)], error_hash, occurrence_count
);
let tags = format!("auto-pattern,fehler,{},{}", tool.to_lowercase(), error_hash);
conn.exec_drop(
r#"INSERT INTO knowledge (category, title, content, tags, priority, status, source, created_at, updated_at)
VALUES ('fehler', ?, ?, ?, 2, 'active', 'auto-pattern', NOW(), NOW())"#,
(&title, &content, &tags),
).await.map_err(|e| e.to_string())?;
let id: i64 = conn.last_insert_id().ok_or("Keine Insert-ID")? as i64;
drop(conn);
let _ = pool.disconnect().await;
println!("🆕 Neues Fehler-Pattern in KB gespeichert: #{} ({}x {})", id, occurrence_count, tool);
Ok(id)
}
// ============ Tauri Commands ============ // ============ Tauri Commands ============
/// Wissensbasis durchsuchen (Volltext) /// Wissensbasis durchsuchen (Volltext)
@ -503,3 +693,28 @@ pub async fn test_knowledge_connection() -> Result<String, String> {
println!("✅ Wissensbasis verbunden: {} Einträge", count); println!("✅ Wissensbasis verbunden: {} Einträge", count);
Ok(format!("Verbunden! {} Einträge in der Wissensbasis", count)) Ok(format!("Verbunden! {} Einträge in der Wissensbasis", count))
} }
// ============ Phase 2.0: Neue Commands ============
/// Keywords aus einer Nachricht extrahieren (für Frontend-Debug/Anzeige)
#[tauri::command]
pub async fn extract_message_keywords(message: String) -> Result<Vec<String>, String> {
Ok(extract_keywords(&message))
}
/// Proaktive KB-Hints bei Session-Start laden
#[tauri::command]
pub async fn get_session_hints(project_name: Option<String>) -> Result<String, String> {
proactive_session_hints(project_name.as_deref()).await
}
/// Fehler-Pattern automatisch in KB speichern (aufgerufen von Frontend bei 3+ Occurrences)
#[tauri::command]
pub async fn auto_save_error_pattern(
error_hash: String,
error_message: String,
tool: String,
occurrence_count: i32,
) -> Result<i64, String> {
save_error_pattern_to_kb(&error_hash, &error_message, &tool, occurrence_count).await
}

View file

@ -33,6 +33,8 @@ pub fn run() {
.manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new()))) .manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new())))
.manage::<hooks::HookState>(Arc::new(Mutex::new(hooks::HookManager::default()))) .manage::<hooks::HookState>(Arc::new(Mutex::new(hooks::HookManager::default())))
.manage::<ide::IdeState>(Arc::new(Mutex::new(ide::IdeConnector::default()))) .manage::<ide::IdeState>(Arc::new(Mutex::new(ide::IdeConnector::default())))
// Phase 2.0: MySQL Pool als Managed State — wird einmal erstellt, von allen Knowledge-Commands geteilt
.manage::<knowledge::MysqlPoolState>(knowledge::create_managed_pool())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
// Claude SDK // Claude SDK
claude::send_message, claude::send_message,
@ -86,6 +88,10 @@ pub fn run() {
db::load_monitor_events_by_type, db::load_monitor_events_by_type,
db::clear_all_monitor_events, db::clear_all_monitor_events,
db::get_monitor_stats, db::get_monitor_stats,
// Phase 2.0: Fehler-Tracking
db::track_error,
db::set_error_kb_pattern,
db::get_error_stats,
// Wissensbasis (claude-db) // Wissensbasis (claude-db)
knowledge::search_knowledge, knowledge::search_knowledge,
knowledge::get_knowledge, knowledge::get_knowledge,
@ -95,6 +101,10 @@ pub fn run() {
knowledge::test_knowledge_connection, knowledge::test_knowledge_connection,
knowledge::get_tool_hints, knowledge::get_tool_hints,
knowledge::format_tool_hints, knowledge::format_tool_hints,
// Phase 2.0: Proaktive Intelligenz
knowledge::extract_message_keywords,
knowledge::get_session_hints,
knowledge::auto_save_error_pattern,
// Context-Management // Context-Management
context::get_sticky_context, context::get_sticky_context,
context::set_sticky_context, context::set_sticky_context,

View file

@ -118,7 +118,7 @@ export async function initEventListeners(): Promise<void> {
}) })
); );
// Session erstellt — Hook feuern (fire-and-forget) // Session erstellt — Hook feuern + proaktive KB-Hints laden (fire-and-forget)
listeners.push( listeners.push(
await listen<{ id: string }>('session-created', (event) => { await listen<{ id: string }>('session-created', (event) => {
const { id } = event.payload; const { id } = event.payload;
@ -127,6 +127,9 @@ export async function initEventListeners(): Promise<void> {
event: 'SessionStart', event: 'SessionStart',
summary: JSON.stringify({ sessionId: id }) summary: JSON.stringify({ sessionId: id })
}).catch((err) => console.debug('Hook session-start fehlgeschlagen:', err)); }).catch((err) => console.debug('Hook session-start fehlgeschlagen:', err));
// Phase 2.0: Proaktive KB-Abfrage bei Session-Start
loadProactiveSessionHints();
}) })
); );
@ -290,6 +293,9 @@ export async function initEventListeners(): Promise<void> {
// Pattern-Detektion ist optional — Fehler nur loggen // Pattern-Detektion ist optional — Fehler nur loggen
console.debug('Pattern-Detektion fehlgeschlagen:', err); console.debug('Pattern-Detektion fehlgeschlagen:', err);
}); });
// Phase 2.0: Auto-Fehler-Tracking — Fehler hashen und zählen
trackErrorOccurrence(output, tool || 'unknown');
} }
}) })
); );
@ -424,6 +430,86 @@ export async function cleanupEventListeners(): Promise<void> {
listeners = []; listeners = [];
} }
// Phase 2.0: Fehler-Hash berechnen (einfacher Hash aus Fehlermeldung)
function hashError(errorMessage: string): string {
// Normalisierung: Zahlen, Pfade und UUIDs entfernen für besseres Grouping
const normalized = errorMessage
.substring(0, 200)
.replace(/\/[\w/.-]+/g, '<PATH>') // Pfade
.replace(/[0-9a-f]{8}-[0-9a-f]{4}/gi, '<UUID>') // UUIDs
.replace(/\d+/g, '<N>') // Zahlen
.replace(/\s+/g, ' ') // Whitespace
.trim()
.toLowerCase();
// Einfacher String-Hash
let hash = 0;
for (let i = 0; i < normalized.length; i++) {
const char = normalized.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 32-bit Integer
}
return 'err_' + Math.abs(hash).toString(36);
}
// Phase 2.0: Auto-Fehler-Tracking — Fehler zählen und bei 3+ automatisch Pattern in KB speichern
async function trackErrorOccurrence(errorMessage: string, tool: string) {
try {
const errorHash = hashError(errorMessage);
const [count, existingKbId] = await invoke<[number, number | null]>('track_error', {
errorHash,
errorMessage: errorMessage.substring(0, 1000),
tool,
});
console.log(`📊 Fehler-Tracking: ${errorHash}${count}x (KB: ${existingKbId || 'noch nicht'})`);
// Bei 3+ Occurrences und noch kein KB-Eintrag: automatisch speichern
if (count >= 3 && !existingKbId) {
console.log(`🆕 Auto-Pattern: Fehler ${count}x aufgetreten, speichere in KB...`);
const kbId = await invoke<number>('auto_save_error_pattern', {
errorHash,
errorMessage: errorMessage.substring(0, 1000),
tool,
occurrenceCount: count,
});
// KB-ID zurückschreiben
await invoke('set_error_kb_pattern', { errorHash, kbPatternId: kbId });
addMonitorEvent('hook', `Auto-Pattern erstellt: KB #${kbId} (${count}x ${tool})`, {
errorHash,
kbId,
occurrenceCount: count,
tool,
});
}
} catch (err) {
// Fehler-Tracking ist komplett optional — niemals die App blockieren
console.debug('Fehler-Tracking fehlgeschlagen:', err);
}
}
// Phase 2.0: Proaktive KB-Abfrage bei Session-Erstellung
export async function loadProactiveSessionHints(projectName?: string): Promise<void> {
try {
const hints = await invoke<string>('get_session_hints', {
projectName: projectName || null,
});
if (hints && hints.length > 0) {
console.log('📋 Proaktive Session-Hints geladen:', hints.length, 'Bytes');
addMonitorEvent('hook', `Proaktive KB-Hints geladen (~${Math.ceil(hints.length / 4)} Token)`, {
projectName,
hintSize: hints.length,
});
}
} catch (err) {
console.debug('Proaktive Session-Hints nicht verfügbar:', err);
}
}
// Agent-Typ mappen // Agent-Typ mappen
function mapAgentType(type: string): Agent['type'] { function mapAgentType(type: string): Agent['type'] {
const typeMap: Record<string, Agent['type']> = { const typeMap: Record<string, Agent['type']> = {