feat(backup): add pre-migration backup, periodic backup, backfill warning, and backup management UI

Four improvements to the database backup mechanism:

1. Auto backup before schema migration - creates a snapshot when
   upgrading from an older database version, providing a safety net
   beyond the existing SAVEPOINT rollback mechanism.

2. Periodic startup backup - checks on app launch whether the latest
   backup is older than 24 hours and creates a new one if needed,
   ensuring all users have recent backups regardless of usage patterns.

3. Backfill failure notification - switch now returns SwitchResult with
   warnings instead of silently ignoring backfill errors, so users are
   informed when their manual config changes may not have been saved.

4. Backup management UI - new BackupListSection in Settings > Data
   Management showing all backup snapshots with restore capability,
   including a confirmation dialog and automatic safety backup before
   restore.
This commit is contained in:
Jason
2026-02-21 23:56:56 +08:00
parent 5ebc879f09
commit 3afec8a10f
18 changed files with 490 additions and 24 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ pub use config::ConfigService;
pub use mcp::McpService;
pub use omo::OmoService;
pub use prompt::PromptService;
pub use provider::{ProviderService, ProviderSortUpdate};
pub use provider::{ProviderService, ProviderSortUpdate, SwitchResult};
pub use proxy::ProxyService;
#[allow(unused_imports)]
pub use skill::{DiscoverableSkill, Skill, SkillRepo, SkillService};
+23 -8
View File
@@ -38,6 +38,13 @@ use usage::validate_usage_script;
/// Provider business logic service
pub struct ProviderService;
/// Result of a provider switch operation, including any non-fatal warnings
#[derive(Debug, serde::Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SwitchResult {
pub warnings: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
@@ -447,7 +454,7 @@ impl ProviderService {
/// c. Update database is_current (as default for new devices)
/// d. Write target provider config to live files
/// e. Sync MCP configuration
pub fn switch(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
pub fn switch(state: &AppState, app_type: AppType, id: &str) -> Result<SwitchResult, AppError> {
// Check if provider exists
let providers = state.db.get_all_providers(app_type.as_str())?;
let _provider = providers
@@ -519,7 +526,7 @@ impl ProviderService {
// Note: No Live config write, no MCP sync
// The proxy server will route requests to the new provider via is_current
return Ok(());
return Ok(SwitchResult::default());
}
// Normal mode: full switch with Live config write
@@ -532,7 +539,7 @@ impl ProviderService {
app_type: AppType,
id: &str,
providers: &indexmap::IndexMap<String, Provider>,
) -> Result<(), AppError> {
) -> Result<SwitchResult, AppError> {
let provider = providers
.get(id)
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
@@ -545,7 +552,7 @@ impl ProviderService {
state,
&crate::services::omo::STANDARD,
)?;
return Ok(());
return Ok(SwitchResult::default());
}
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo-slim")
@@ -554,9 +561,11 @@ impl ProviderService {
.db
.set_omo_provider_current(app_type.as_str(), id, "omo-slim")?;
crate::services::OmoService::write_config_to_file(state, &crate::services::omo::SLIM)?;
return Ok(());
return Ok(SwitchResult::default());
}
let mut result = SwitchResult::default();
// Backfill: Backfill current live config to current provider
// Use effective current provider (validated existence) to ensure backfill targets valid provider
let current_id = crate::settings::get_effective_current_provider(&state.db, &app_type)?;
@@ -570,8 +579,14 @@ impl ProviderService {
if let Ok(live_config) = read_live_settings(app_type.clone()) {
if let Some(mut current_provider) = providers.get(&current_id).cloned() {
current_provider.settings_config = live_config;
// Ignore backfill failure, don't affect switch flow
let _ = state.db.save_provider(app_type.as_str(), &current_provider);
if let Err(e) =
state.db.save_provider(app_type.as_str(), &current_provider)
{
log::warn!("Backfill failed: {e}");
result
.warnings
.push(format!("backfill_failed:{current_id}"));
}
}
}
}
@@ -593,7 +608,7 @@ impl ProviderService {
// Sync MCP
McpService::sync_all_enabled(state)?;
Ok(())
Ok(result)
}
/// Sync current provider to live configuration (re-export)