feat(classroom): add SQLite persistence + security hardening
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

M11-03: Classroom data persistence
- New persist.rs: SQLite-backed ClassroomPersistence with open/load_all/save
- Schema: classrooms (JSON blob) + classroom_chats tables
- generate.rs: auto-persist classroom after generation
- chat.rs: auto-persist chat messages after each exchange
- mod.rs: init_persistence() for app setup integration

M1-01: Gemini API key now uses x-goog-api-key header
- No longer leaks API key in URL query params or debug logs

M1-03/04: Mutex unwrap() replaced with unwrap_or_else(|e| e.into_inner())
- MemoryMiddleware and LoopGuardMiddleware recover from poison

M2-08: Agent creation input validation
- Reject empty names, out-of-range temperature (0-2), zero max_tokens

M11-06: Classroom chat message ID uses crypto.randomUUID()
This commit is contained in:
iven
2026-04-04 19:26:59 +08:00
parent 619bad30cb
commit 88172aa651
4 changed files with 207 additions and 1 deletions

View File

@@ -7,11 +7,13 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use tauri::Manager;
use zclaw_kernel::generation::Classroom;
pub mod chat;
pub mod export;
pub mod generate;
pub mod persist;
// ---------------------------------------------------------------------------
// Shared state types
@@ -27,6 +29,7 @@ pub type GenerationTasks = Arc<Mutex<std::collections::HashMap<String, zclaw_ker
// Re-export chat state type — used by lib.rs to construct managed state
#[allow(unused_imports)]
pub use chat::ChatStore;
pub use persist::ClassroomPersistence;
// ---------------------------------------------------------------------------
// State constructors
@@ -39,3 +42,24 @@ pub fn create_classroom_state() -> ClassroomStore {
pub fn create_generation_tasks() -> GenerationTasks {
Arc::new(Mutex::new(std::collections::HashMap::new()))
}
/// Create and initialize the classroom persistence layer, loading saved data.
pub async fn init_persistence(
app_handle: &tauri::AppHandle,
store: &ClassroomStore,
chat_store: &ChatStore,
) -> Result<ClassroomPersistence, String> {
let app_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| format!("Failed to get app data dir: {}", e))?;
let db_path = app_dir.join("classroom").join("classrooms.db");
std::fs::create_dir_all(db_path.parent().unwrap())
.map_err(|e| format!("Failed to create classroom dir: {}", e))?;
let persistence: ClassroomPersistence = ClassroomPersistence::open(db_path).await?;
let loaded = persistence.load_all(store, chat_store).await?;
tracing::info!("[ClassroomPersistence] Loaded {} classrooms from SQLite", loaded);
Ok(persistence)
}