- 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>
274 lines
9.6 KiB
Rust
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))
|
|
}
|