chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -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);