diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 51c75f4..d790765 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1493,7 +1493,7 @@ impl SessionStore { // Insert new todos for item in items { conn.execute( - "INSERT INTO todos (id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at) + "INSERT OR REPLACE INTO todos (id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", params![ item.id, @@ -1904,7 +1904,7 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> { conn.execute_batch( " CREATE TABLE IF NOT EXISTS todos ( - id TEXT PRIMARY KEY, + id TEXT NOT NULL, scope_key TEXT NOT NULL, session_id TEXT NOT NULL, topic_id TEXT, @@ -1912,7 +1912,8 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> { status TEXT NOT NULL DEFAULT 'pending', priority TEXT NOT NULL DEFAULT 'medium', created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL + updated_at INTEGER NOT NULL, + PRIMARY KEY (id, scope_key) ); CREATE INDEX IF NOT EXISTS idx_todos_scope @@ -1922,6 +1923,55 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> { ON todos(session_id); ", )?; + return Ok(()); + } + + // Migration: check if old schema has single-column PRIMARY KEY on `id` + // If so, migrate to composite PRIMARY KEY (id, scope_key) + let sql: String = conn + .query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='todos'", + [], + |row| row.get::<_, String>(0), + ) + .unwrap_or_default(); + + let needs_migration = sql.contains("id TEXT PRIMARY KEY") + || (sql.contains("PRIMARY KEY") && !sql.contains("PRIMARY KEY (id, scope_key)")); + + if needs_migration { + tracing::info!("Migrating todos table to composite PRIMARY KEY (id, scope_key)"); + conn.execute_batch( + " + CREATE TABLE todos_new ( + id TEXT NOT NULL, + scope_key TEXT NOT NULL, + session_id TEXT NOT NULL, + topic_id TEXT, + content TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + priority TEXT NOT NULL DEFAULT 'medium', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (id, scope_key) + ); + + INSERT OR IGNORE INTO todos_new + SELECT id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at + FROM todos; + + DROP TABLE todos; + + ALTER TABLE todos_new RENAME TO todos; + + CREATE INDEX IF NOT EXISTS idx_todos_scope + ON todos(scope_key, created_at ASC); + + CREATE INDEX IF NOT EXISTS idx_todos_session + ON todos(session_id); + ", + )?; + tracing::info!("Todos table migration complete"); } Ok(()) diff --git a/web/src/components/Settings/ConfigPage.tsx b/web/src/components/Settings/ConfigPage.tsx index 7d8afbb..43793a1 100644 --- a/web/src/components/Settings/ConfigPage.tsx +++ b/web/src/components/Settings/ConfigPage.tsx @@ -165,6 +165,11 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) { const [toast, setToast] = useState('') const [dirty, setDirty] = useState(false) + const handleClose = useCallback(() => { + if (dirty && !confirm('有未保存的更改,确定要关闭吗?')) return + onClose() + }, [dirty, onClose]) + // Load config useEffect(() => { fetch('/api/config').then(r => r.json()).then(data => { @@ -175,10 +180,10 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) { // ESC to close useEffect(() => { - const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } + const h = (e: KeyboardEvent) => { if (e.key === 'Escape') handleClose() } document.addEventListener('keydown', h) return () => document.removeEventListener('keydown', h) - }, [onClose]) + }, [handleClose]) const update = useCallback((key: K, value: AppConfig[K]) => { setConfig(prev => prev ? { ...prev, [key]: value } : prev) @@ -589,7 +594,7 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) { } return ( -
+
e.stopPropagation()} @@ -600,7 +605,7 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) { 系统配置 {dirty && 未保存}
-
@@ -640,7 +645,7 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
{error && {error}}
-