chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -289,6 +289,44 @@ impl sqlx::FromRow<'_, SqliteRow> for MemoryRow {
|
||||
}
|
||||
}
|
||||
|
||||
/// Private helper methods on SqliteStorage (NOT in impl VikingStorage block)
|
||||
impl SqliteStorage {
|
||||
/// Fetch memories by scope with importance-based ordering.
|
||||
/// Used internally by find() for scope-based queries.
|
||||
pub(crate) async fn fetch_by_scope_priv(&self, scope: Option<&str>, limit: usize) -> Result<Vec<MemoryRow>> {
|
||||
let rows = if let Some(scope) = scope {
|
||||
sqlx::query_as::<_, MemoryRow>(
|
||||
r#"
|
||||
SELECT uri, memory_type, content, keywords, importance, access_count, created_at, last_accessed, overview, abstract_summary
|
||||
FROM memories
|
||||
WHERE uri LIKE ?
|
||||
ORDER BY importance DESC, access_count DESC
|
||||
LIMIT ?
|
||||
"#
|
||||
)
|
||||
.bind(format!("{}%", scope))
|
||||
.bind(limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(format!("Failed to fetch by scope: {}", e)))?
|
||||
} else {
|
||||
sqlx::query_as::<_, MemoryRow>(
|
||||
r#"
|
||||
SELECT uri, memory_type, content, keywords, importance, access_count, created_at, last_accessed, overview, abstract_summary
|
||||
FROM memories
|
||||
ORDER BY importance DESC
|
||||
LIMIT ?
|
||||
"#
|
||||
)
|
||||
.bind(limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(format!("Failed to fetch by scope: {}", e)))?
|
||||
};
|
||||
Ok(rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VikingStorage for SqliteStorage {
|
||||
async fn store(&self, entry: &MemoryEntry) -> Result<()> {
|
||||
@@ -374,22 +412,61 @@ impl VikingStorage for SqliteStorage {
|
||||
}
|
||||
|
||||
async fn find(&self, query: &str, options: FindOptions) -> Result<Vec<MemoryEntry>> {
|
||||
// Get all matching entries
|
||||
let rows = if let Some(ref scope) = options.scope {
|
||||
sqlx::query_as::<_, MemoryRow>(
|
||||
"SELECT uri, memory_type, content, keywords, importance, access_count, created_at, last_accessed, overview, abstract_summary FROM memories WHERE uri LIKE ?"
|
||||
)
|
||||
.bind(format!("{}%", scope))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(format!("Failed to find memories: {}", e)))?
|
||||
let limit = options.limit.unwrap_or(50).max(20); // Fetch more candidates for reranking
|
||||
|
||||
// Strategy: use FTS5 for initial filtering when query is non-empty,
|
||||
// then score candidates with TF-IDF / embedding for precise ranking.
|
||||
// Fallback to scope-only scan when query is empty (e.g., "list all").
|
||||
let rows = if !query.is_empty() {
|
||||
// FTS5-powered candidate retrieval (fast, index-based)
|
||||
let fts_candidates = if let Some(ref scope) = options.scope {
|
||||
sqlx::query_as::<_, MemoryRow>(
|
||||
r#"
|
||||
SELECT m.uri, m.memory_type, m.content, m.keywords, m.importance,
|
||||
m.access_count, m.created_at, m.last_accessed, m.overview, m.abstract_summary
|
||||
FROM memories m
|
||||
INNER JOIN memories_fts f ON m.uri = f.uri
|
||||
WHERE f.memories_fts MATCH ?
|
||||
AND m.uri LIKE ?
|
||||
ORDER BY f.rank
|
||||
LIMIT ?
|
||||
"#
|
||||
)
|
||||
.bind(query)
|
||||
.bind(format!("{}%", scope))
|
||||
.bind(limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
} else {
|
||||
sqlx::query_as::<_, MemoryRow>(
|
||||
r#"
|
||||
SELECT m.uri, m.memory_type, m.content, m.keywords, m.importance,
|
||||
m.access_count, m.created_at, m.last_accessed, m.overview, m.abstract_summary
|
||||
FROM memories m
|
||||
INNER JOIN memories_fts f ON m.uri = f.uri
|
||||
WHERE f.memories_fts MATCH ?
|
||||
ORDER BY f.rank
|
||||
LIMIT ?
|
||||
"#
|
||||
)
|
||||
.bind(query)
|
||||
.bind(limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
};
|
||||
|
||||
match fts_candidates {
|
||||
Ok(rows) if !rows.is_empty() => rows,
|
||||
Ok(_) | Err(_) => {
|
||||
// FTS5 returned nothing or query syntax was invalid —
|
||||
// fallback to scope-based scan (no full table scan unless no scope)
|
||||
tracing::debug!("[SqliteStorage] FTS5 returned no results, falling back to scope scan");
|
||||
self.fetch_by_scope_priv(options.scope.as_deref(), limit).await?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlx::query_as::<_, MemoryRow>(
|
||||
"SELECT uri, memory_type, content, keywords, importance, access_count, created_at, last_accessed, overview, abstract_summary FROM memories"
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(format!("Failed to find memories: {}", e)))?
|
||||
// Empty query: scope-based scan only (no FTS5 needed)
|
||||
self.fetch_by_scope_priv(options.scope.as_deref(), limit).await?
|
||||
};
|
||||
|
||||
// Convert to entries and compute semantic scores
|
||||
@@ -464,16 +541,8 @@ impl VikingStorage for SqliteStorage {
|
||||
}
|
||||
|
||||
async fn find_by_prefix(&self, prefix: &str) -> Result<Vec<MemoryEntry>> {
|
||||
let rows = sqlx::query_as::<_, MemoryRow>(
|
||||
"SELECT uri, memory_type, content, keywords, importance, access_count, created_at, last_accessed, overview, abstract_summary FROM memories WHERE uri LIKE ?"
|
||||
)
|
||||
.bind(format!("{}%", prefix))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(format!("Failed to find by prefix: {}", e)))?;
|
||||
|
||||
let rows = self.fetch_by_scope_priv(Some(prefix), 100).await?;
|
||||
let entries = rows.iter().map(|row| self.row_to_entry(row)).collect();
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
@@ -484,13 +553,13 @@ impl VikingStorage for SqliteStorage {
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(format!("Failed to delete memory: {}", e)))?;
|
||||
|
||||
// Remove from FTS
|
||||
// Remove from FTS index
|
||||
let _ = sqlx::query("DELETE FROM memories_fts WHERE uri = ?")
|
||||
.bind(uri)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
// Remove from scorer
|
||||
// Remove from in-memory scorer
|
||||
let mut scorer = self.scorer.write().await;
|
||||
scorer.remove_entry(uri);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user