feat: complete Phase 1-3 architecture optimization

Phase 1 - Security:
- Add AES-GCM encryption for localStorage fallback
- Enforce WSS protocol for non-localhost WebSocket connections
- Add URL sanitization to prevent XSS in markdown links

Phase 2 - Domain Reorganization:
- Create Intelligence Domain with Valtio store and caching
- Add unified intelligence-client for Rust backend integration
- Migrate from legacy agent-memory, heartbeat, reflection modules

Phase 3 - Core Optimization:
- Add virtual scrolling for ChatArea with react-window
- Implement LRU cache with TTL for intelligence operations
- Add message virtualization utilities

Additional:
- Add OpenFang compatibility test suite
- Update E2E test fixtures
- Add audit logging infrastructure
- Update project documentation and plans

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-21 22:11:50 +08:00
parent 815c56326b
commit ce562e8bfc
36 changed files with 5241 additions and 201 deletions

View File

@@ -13,6 +13,8 @@ use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use uuid::Uuid;
use tauri::Manager;
use sqlx::{SqliteConnection, Connection, Row, sqlite::SqliteRow};
use chrono::{DateTime, Utc};
/// Memory entry stored in SQLite
@@ -32,6 +34,26 @@ pub struct PersistentMemory {
pub embedding: Option<Vec<u8>>, // Vector embedding for semantic search
}
// Manual implementation of FromRow since sqlx::FromRow derive has issues with Option<Vec<u8>>
impl<'r> sqlx::FromRow<'r, SqliteRow> for PersistentMemory {
fn from_row(row: &'r SqliteRow) -> Result<Self, sqlx::Error> {
Ok(PersistentMemory {
id: row.try_get("id")?,
agent_id: row.try_get("agent_id")?,
memory_type: row.try_get("memory_type")?,
content: row.try_get("content")?,
importance: row.try_get("importance")?,
source: row.try_get("source")?,
tags: row.try_get("tags")?,
conversation_id: row.try_get("conversation_id")?,
created_at: row.try_get("created_at")?,
last_accessed_at: row.try_get("last_accessed_at")?,
access_count: row.try_get("access_count")?,
embedding: row.try_get("embedding")?,
})
}
}
/// Memory search options
#[derive(Debug, Clone)]
pub struct MemorySearchQuery {
@@ -58,7 +80,7 @@ pub struct MemoryStats {
/// Persistent memory store backed by SQLite
pub struct PersistentMemoryStore {
path: PathBuf,
conn: Arc<Mutex<sqlx::SqliteConnection>>,
conn: Arc<Mutex<SqliteConnection>>,
}
impl PersistentMemoryStore {
@@ -80,10 +102,8 @@ impl PersistentMemoryStore {
/// Open an existing memory store
pub async fn open(path: PathBuf) -> Result<Self, String> {
let conn = sqlx::sqlite::SqliteConnectOptions::new()
.filename(&path)
.create_if_missing(true)
.connect(sqlx::sqlite::SqliteConnectOptions::path)
let db_url = format!("sqlite:{}?mode=rwc", path.display());
let conn = SqliteConnection::connect(&db_url)
.await
.map_err(|e| format!("Failed to open database: {}", e))?;
@@ -99,7 +119,7 @@ impl PersistentMemoryStore {
/// Initialize the database schema
async fn init_schema(&self) -> Result<(), String> {
let conn = self.conn.lock().await;
let mut conn = self.conn.lock().await;
sqlx::query(
r#"
@@ -124,7 +144,7 @@ impl PersistentMemoryStore {
CREATE INDEX IF NOT EXISTS idx_importance ON memories(importance);
"#,
)
.execute(&*conn)
.execute(&mut *conn)
.await
.map_err(|e| format!("Failed to create schema: {}", e))?;
@@ -133,7 +153,7 @@ impl PersistentMemoryStore {
/// Store a new memory
pub async fn store(&self, memory: &PersistentMemory) -> Result<(), String> {
let conn = self.conn.lock().await;
let mut conn = self.conn.lock().await;
sqlx::query(
r#"
@@ -156,7 +176,7 @@ impl PersistentMemoryStore {
.bind(&memory.last_accessed_at)
.bind(memory.access_count)
.bind(&memory.embedding)
.execute(&*conn)
.execute(&mut *conn)
.await
.map_err(|e| format!("Failed to store memory: {}", e))?;
@@ -165,13 +185,13 @@ impl PersistentMemoryStore {
/// Get a memory by ID
pub async fn get(&self, id: &str) -> Result<Option<PersistentMemory>, String> {
let conn = self.conn.lock().await;
let mut conn = self.conn.lock().await;
let result = sqlx::query_as::<_, PersistentMemory>(
let result: Option<PersistentMemory> = sqlx::query_as(
"SELECT * FROM memories WHERE id = ?",
)
.bind(id)
.fetch_optional(&*conn)
.fetch_optional(&mut *conn)
.await
.map_err(|e| format!("Failed to get memory: {}", e))?;
@@ -183,7 +203,7 @@ impl PersistentMemoryStore {
)
.bind(&now)
.bind(id)
.execute(&*conn)
.execute(&mut *conn)
.await
.ok();
}
@@ -191,50 +211,51 @@ impl PersistentMemoryStore {
Ok(result)
}
/// Search memories
/// Search memories with simple query
pub async fn search(&self, query: MemorySearchQuery) -> Result<Vec<PersistentMemory>, String> {
let conn = self.conn.lock().await;
let mut conn = self.conn.lock().await;
let mut sql = String::from("SELECT * FROM memories WHERE 1=1");
let mut bindings: Vec<Box<dyn sqlx::Encode + sqlx::Type<_>>> = Vec::new();
let mut params: Vec<String> = Vec::new();
if let Some(agent_id) = &query.agent_id {
sql.push_str(" AND agent_id = ?");
bindings.push(Box::new(agent_id.to_string()));
params.push(agent_id.clone());
}
if let Some(memory_type) = &query.memory_type {
sql.push_str(" AND memory_type = ?");
bindings.push(Box::new(memory_type.to_string()));
params.push(memory_type.clone());
}
if let Some(min_importance) = &query.min_importance {
if let Some(min_importance) = query.min_importance {
sql.push_str(" AND importance >= ?");
bindings.push(Box::new(min_importance));
params.push(min_importance.to_string());
}
if let Some(q) = &query.query {
if let Some(query_text) = &query.query {
sql.push_str(" AND content LIKE ?");
bindings.push(Box::new(format!("%{}%", q)));
params.push(format!("%{}%", query_text));
}
sql.push_str(" ORDER BY importance DESC, created_at DESC");
sql.push_str(" ORDER BY created_at DESC");
if let Some(limit) = &query.limit {
if let Some(limit) = query.limit {
sql.push_str(&format!(" LIMIT {}", limit));
}
if let Some(offset) = &query.offset {
if let Some(offset) = query.offset {
sql.push_str(&format!(" OFFSET {}", offset));
}
// Build and execute query dynamically
let mut query_builder = sqlx::query_as::<_, PersistentMemory>(&sql);
for binding in bindings {
query_builder = query_builder.bind(binding);
for param in params {
query_builder = query_builder.bind(param);
}
let results = query_builder
.fetch_all(&*conn)
.fetch_all(&mut *conn)
.await
.map_err(|e| format!("Failed to search memories: {}", e))?;
@@ -242,79 +263,80 @@ impl PersistentMemoryStore {
}
/// Delete a memory by ID
pub async fn delete(&self, id: &str) -> Result<(), String> {
let conn = self.conn.lock().await;
pub async fn delete(&self, id: &str) -> Result<bool, String> {
let mut conn = self.conn.lock().await;
sqlx::query("DELETE FROM memories WHERE id = ?")
let result = sqlx::query("DELETE FROM memories WHERE id = ?")
.bind(id)
.execute(&*conn)
.execute(&mut *conn)
.await
.map_err(|e| format!("Failed to delete memory: {}", e))?;
Ok(())
Ok(result.rows_affected() > 0)
}
/// Delete all memories for an agent
pub async fn delete_all_for_agent(&self, agent_id: &str) -> Result<usize, String> {
let conn = self.conn.lock().await;
pub async fn delete_by_agent(&self, agent_id: &str) -> Result<usize, String> {
let mut conn = self.conn.lock().await;
let result = sqlx::query("DELETE FROM memories WHERE agent_id = ?")
.bind(agent_id)
.execute(&*conn)
.execute(&mut *conn)
.await
.map_err(|e| format!("Failed to delete agent memories: {}", e))?;
Ok(result.rows_affected())
Ok(result.rows_affected() as usize)
}
/// Get memory statistics
pub async fn stats(&self) -> Result<MemoryStats, String> {
let conn = self.conn.lock().await;
let mut conn = self.conn.lock().await;
let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM memories")
.fetch_one(&*conn)
.fetch_one(&mut *conn)
.await
.unwrap_or(0);
let by_type: std::collections::HashMap<String, i64> = sqlx::query_as(
"SELECT memory_type, COUNT(*) as count FROM memories GROUP BY memory_type",
)
.fetch_all(&*conn)
.fetch_all(&mut *conn)
.await
.unwrap_or_default()
.into_iter()
.map(|(memory_type, count)| (memory_type, count))
.map(|row: (String, i64)| row)
.collect();
let by_agent: std::collections::HashMap<String, i64> = sqlx::query_as(
"SELECT agent_id, COUNT(*) as count FROM memories GROUP BY agent_id",
)
.fetch_all(&*conn)
.fetch_all(&mut *conn)
.await
.unwrap_or_default()
.into_iter()
.map(|(agent_id, count)| (agent_id, count))
.map(|row: (String, i64)| row)
.collect();
let oldest: Option<String> = sqlx::query_scalar(
"SELECT MIN(created_at) FROM memories",
)
.fetch_optional(&*conn)
.fetch_optional(&mut *conn)
.await
.unwrap_or_default();
let newest: Option<String> = sqlx::query_scalar(
"SELECT MAX(created_at) FROM memories",
)
.fetch_optional(&*conn)
.fetch_optional(&mut *conn)
.await
.unwrap_or_default();
let storage_size: i64 = sqlx::query_scalar(
"SELECT SUM(LENGTH(content) + LENGTH(tags) + COALESCE(LENGTH(embedding), 0)) FROM memories",
)
.fetch_one(&*conn)
.fetch_optional(&mut *conn)
.await
.unwrap_or(Some(0))
.unwrap_or(0);
Ok(MemoryStats {
@@ -329,12 +351,12 @@ impl PersistentMemoryStore {
/// Export memories for backup
pub async fn export_all(&self) -> Result<Vec<PersistentMemory>, String> {
let conn = self.conn.lock().await;
let mut conn = self.conn.lock().await;
let memories = sqlx::query_as::<_, PersistentMemory>(
"SELECT * FROM memories ORDER BY created_at ASC",
)
.fetch_all(&*conn)
.fetch_all(&mut *conn)
.await
.map_err(|e| format!("Failed to export memories: {}", e))?;
@@ -353,24 +375,24 @@ impl PersistentMemoryStore {
/// Get the database path
pub fn path(&self) -> &PathBuf {
self.path.clone()
&self.path
}
}
/// Generate a unique memory ID
pub fn generate_memory_id() -> String {
format!("mem_{}_{}", Utc::now().timestamp(), Uuid::new_v4().to_string().replace("-", "").substring(0, 8))
let uuid_str = Uuid::new_v4().to_string().replace("-", "");
let short_uuid = &uuid_str[..8];
format!("mem_{}_{}", Utc::now().timestamp(), short_uuid)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_memory_store() {
// This would require a test database setup
// For now, just verify the struct compiles
let _ = generate_memory_id();
assert!(_memory_id.starts_with("mem_"));
#[test]
fn test_generate_memory_id() {
let memory_id = generate_memory_id();
assert!(memory_id.starts_with("mem_"));
}
}