claude-desktop/src-tauri/src/knowledge.rs
Eddy e6bd0de3da Phase 8: Claude-DB Integration — Wissensbasis-Anbindung
- knowledge.rs: MySQL-Verbindung zu claude-db (192.168.155.1)
- Volltextsuche mit MATCH AGAINST
- "Das merken" Feature zum Speichern
- KnowledgePanel.svelte: Suche, Filter, Detail-View
- Neuer Tab "Wissen" im mittleren Panel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-14 13:27:59 +02:00

274 lines
9.6 KiB
Rust

// Claude Desktop — Wissensbasis (claude-db)
// Direkte MySQL-Anbindung zur zentralen Wissensdatenbank
use mysql_async::{Pool, prelude::*};
use serde::{Deserialize, Serialize};
/// Verbindungskonfiguration
const MYSQL_HOST: &str = "192.168.155.1";
const MYSQL_PORT: u16 = 3306;
const MYSQL_USER: &str = "claude";
const MYSQL_PASS: &str = "claude";
const MYSQL_DB: &str = "claude";
/// Wissenseintrag aus der knowledge-Tabelle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeEntry {
pub id: i64,
pub category: String,
pub title: String,
pub content: String,
pub tags: Option<String>,
pub priority: i32,
pub status: String,
pub related_ids: Option<String>,
pub source: Option<String>,
pub created_at: String,
pub updated_at: String,
}
/// Neuer Wissenseintrag (ohne id, timestamps)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewKnowledge {
pub category: String,
pub title: String,
pub content: String,
pub tags: Option<String>,
pub priority: Option<i32>,
pub source: Option<String>,
}
/// Suchergebnis mit Relevanz-Score
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub entry: KnowledgeEntry,
pub relevance: f64,
}
/// Verfügbare Kategorien
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryInfo {
pub name: String,
pub count: i64,
}
// ============ Hilfsfunktionen ============
/// Erstellt einen MySQL Connection-Pool
fn create_pool() -> Pool {
let url = format!(
"mysql://{}:{}@{}:{}/{}",
MYSQL_USER, MYSQL_PASS, MYSQL_HOST, MYSQL_PORT, MYSQL_DB
);
Pool::new(url.as_str())
}
// ============ Tauri Commands ============
/// Wissensbasis durchsuchen (Volltext)
#[tauri::command]
pub async fn search_knowledge(
query: String,
category: Option<String>,
limit: Option<usize>,
) -> Result<Vec<SearchResult>, String> {
let pool = create_pool();
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
let limit = limit.unwrap_or(20);
// Volltext-Suche mit optionalem Kategorie-Filter
let results: Vec<SearchResult> = if let Some(cat) = category {
conn.exec_map(
r#"SELECT
id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at,
MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) as relevance
FROM knowledge
WHERE MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
AND category = ?
ORDER BY relevance DESC
LIMIT ?"#,
(&query, &query, &cat, limit),
|(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at, relevance):
(i64, String, String, String, Option<String>, i32, String, Option<String>, Option<String>, String, String, f64)| {
SearchResult {
entry: KnowledgeEntry {
id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at,
},
relevance,
}
}
).await.map_err(|e| e.to_string())?
} else {
conn.exec_map(
r#"SELECT
id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at,
MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) as relevance
FROM knowledge
WHERE MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC
LIMIT ?"#,
(&query, &query, limit),
|(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at, relevance):
(i64, String, String, String, Option<String>, i32, String, Option<String>, Option<String>, String, String, f64)| {
SearchResult {
entry: KnowledgeEntry {
id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at,
},
relevance,
}
}
).await.map_err(|e| e.to_string())?
};
drop(conn);
pool.disconnect().await.map_err(|e| e.to_string())?;
println!("🔍 Wissensbasis-Suche '{}': {} Treffer", query, results.len());
Ok(results)
}
/// Wissenseintrag nach ID laden
#[tauri::command]
pub async fn get_knowledge(id: i64) -> Result<Option<KnowledgeEntry>, String> {
let pool = create_pool();
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
let result: Option<KnowledgeEntry> = conn.exec_first(
r#"SELECT id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at
FROM knowledge WHERE id = ?"#,
(id,)
).await.map_err(|e| e.to_string())?
.map(|(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at):
(i64, String, String, String, Option<String>, i32, String, Option<String>, Option<String>, String, String)| {
KnowledgeEntry {
id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at,
}
});
drop(conn);
pool.disconnect().await.map_err(|e| e.to_string())?;
Ok(result)
}
/// Neuen Wissenseintrag speichern ("Das merken")
#[tauri::command]
pub async fn save_knowledge(entry: NewKnowledge) -> Result<i64, String> {
let pool = create_pool();
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
let priority = entry.priority.unwrap_or(2);
conn.exec_drop(
r#"INSERT INTO knowledge (category, title, content, tags, priority, status, source, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'active', ?, NOW(), NOW())"#,
(&entry.category, &entry.title, &entry.content, &entry.tags, priority, &entry.source)
).await.map_err(|e| e.to_string())?;
let id: i64 = conn.last_insert_id().ok_or("Keine Insert-ID")? as i64;
drop(conn);
pool.disconnect().await.map_err(|e| e.to_string())?;
println!("💾 Wissen gespeichert: {} (ID: {})", entry.title, id);
Ok(id)
}
/// Alle Kategorien mit Anzahl der Einträge
#[tauri::command]
pub async fn get_knowledge_categories() -> Result<Vec<CategoryInfo>, String> {
let pool = create_pool();
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
let categories: Vec<CategoryInfo> = conn.query_map(
r#"SELECT category, COUNT(*) as count
FROM knowledge
WHERE status = 'active'
GROUP BY category
ORDER BY count DESC"#,
|(name, count): (String, i64)| CategoryInfo { name, count }
).await.map_err(|e| e.to_string())?;
drop(conn);
pool.disconnect().await.map_err(|e| e.to_string())?;
Ok(categories)
}
/// Letzte N Wissenseinträge laden
#[tauri::command]
pub async fn get_recent_knowledge(
limit: Option<usize>,
category: Option<String>,
) -> Result<Vec<KnowledgeEntry>, String> {
let pool = create_pool();
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
let limit = limit.unwrap_or(10);
let entries: Vec<KnowledgeEntry> = if let Some(cat) = category {
conn.exec_map(
r#"SELECT id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at
FROM knowledge
WHERE status = 'active' AND category = ?
ORDER BY updated_at DESC
LIMIT ?"#,
(&cat, limit),
|(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at):
(i64, String, String, String, Option<String>, i32, String, Option<String>, Option<String>, String, String)| {
KnowledgeEntry {
id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at,
}
}
).await.map_err(|e| e.to_string())?
} else {
conn.exec_map(
r#"SELECT id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at
FROM knowledge
WHERE status = 'active'
ORDER BY updated_at DESC
LIMIT ?"#,
(limit,),
|(id, category, title, content, tags, priority, status, related_ids, source, created_at, updated_at):
(i64, String, String, String, Option<String>, i32, String, Option<String>, Option<String>, String, String)| {
KnowledgeEntry {
id, category, title, content, tags, priority, status,
related_ids, source, created_at, updated_at,
}
}
).await.map_err(|e| e.to_string())?
};
drop(conn);
pool.disconnect().await.map_err(|e| e.to_string())?;
Ok(entries)
}
/// Verbindung zur Wissensbasis testen
#[tauri::command]
pub async fn test_knowledge_connection() -> Result<String, String> {
let pool = create_pool();
let mut conn = pool.get_conn().await.map_err(|e| format!("Verbindungsfehler: {}", e))?;
let count: i64 = conn.query_first("SELECT COUNT(*) FROM knowledge")
.await
.map_err(|e| e.to_string())?
.unwrap_or(0);
drop(conn);
pool.disconnect().await.map_err(|e| e.to_string())?;
println!("✅ Wissensbasis verbunden: {} Einträge", count);
Ok(format!("Verbunden! {} Einträge in der Wissensbasis", count))
}