KB multi-article support: CRUD articles, per-article indexing, sidebar KB mode
- Replace singleton kb_content table with kb_articles (id, title, content) - Add article_id to kb_chunks for per-article chunk tracking - Auto-migrate old kb_content data on startup - KbManager: index/delete per article, search across all with article_title - API: full CRUD on /kb/articles, keep GET /kb for agent tool - Agent: kb_search shows article labels, kb_read concatenates all articles - Frontend: Sidebar KB mode with article list, KbEditor for single article Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
61
src/db.rs
61
src/db.rs
@@ -103,8 +103,9 @@ impl Database {
|
||||
|
||||
// KB tables
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS kb_content (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
"CREATE TABLE IF NOT EXISTS kb_articles (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)"
|
||||
@@ -112,16 +113,10 @@ impl Database {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// Insert default row if not exists
|
||||
let _ = sqlx::query(
|
||||
"INSERT OR IGNORE INTO kb_content (id, content) VALUES (1, '')"
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS kb_chunks (
|
||||
id TEXT PRIMARY KEY,
|
||||
article_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
embedding BLOB NOT NULL
|
||||
@@ -130,6 +125,46 @@ impl Database {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// Migration: add article_id to kb_chunks if missing
|
||||
let _ = sqlx::query(
|
||||
"ALTER TABLE kb_chunks ADD COLUMN article_id TEXT NOT NULL DEFAULT ''"
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
// Migrate old kb_content to kb_articles
|
||||
let has_old_table: bool = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='kb_content'"
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_old_table {
|
||||
let old_content: Option<String> = sqlx::query_scalar(
|
||||
"SELECT content FROM kb_content WHERE id = 1"
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if let Some(content) = old_content {
|
||||
if !content.is_empty() {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let _ = sqlx::query(
|
||||
"INSERT OR IGNORE INTO kb_articles (id, title, content) VALUES (?, '导入的知识库', ?)"
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&content)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
let _ = sqlx::query("DROP TABLE kb_content")
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS timers (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -192,6 +227,14 @@ pub struct Comment {
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct KbArticle {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Timer {
|
||||
pub id: String,
|
||||
|
||||
Reference in New Issue
Block a user