Skip to content

Commit a724bd1

Browse files
committed
fix(ai): M4 既有用户默认链升级——SiliconFlow 默认迁移到 DeepSeek
真机截图取证:既有安装(ai_providers.json 已存在)的默认 Provider 仍是 SiliconFlow——原迁移仅在 providers 为空(新装)时触发,M4"默认 Provider=DeepSeek" 对既有用户未生效。 - ai_provider 新增纯函数 upgrade_existing_default_to_deepseek:默认 Provider 为旧 SiliconFlow 链(siliconflow/legacy-siliconflow)且尚无 deepseek 时,插入 DeepSeek 预设并设为默认;旧 Provider 保留(密钥不受影响,可回退/降级链) - 保守边界:用户显式设其他默认(openrouter/自定义)不覆盖;4 场景单测(升级/幂等/ 自定义默认/未设默认) - app_setup 接线:providers 非空且升级返回 true 时持久化
1 parent c9e3731 commit a724bd1

3 files changed

Lines changed: 86 additions & 0 deletions

File tree

‎app/src-tauri/src/ai_provider.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,35 @@ pub fn migrate_from_legacy(_legacy: &crate::ai_settings::AiSettings) -> (Vec<AiP
218218
(vec![p], Some(default_id))
219219
}
220220

221+
/// v0.12.0 M4:既有用户默认链升级(纯函数,返回是否变更)。
222+
///
223+
/// @ai-context: 首启迁移只在 providers 为空时触发(新装/迁移)——**既有安装**
224+
/// (ai_providers.json 已存在)的默认 Provider 仍是旧 SiliconFlow 链,
225+
/// 与"默认 Provider=DeepSeek"的 M4 目标不符(真机截图取证)。
226+
/// 本函数:默认 Provider 为旧链(siliconflow/legacy-siliconflow)且
227+
/// 尚无 deepseek Provider 时,插入 DeepSeek 预设并设为默认——
228+
/// 旧 Provider 保留(密钥不受影响,可回退/降级链使用)。
229+
/// @ai-context: 保守边界——用户显式改为其他默认(openrouter/自定义)不覆盖;
230+
/// 仅升级一次的守卫 = "无 deepseek Provider"(用户主动删除 DeepSeek
231+
/// 并重回 SiliconFlow 默认时下次启动会重新插入——可接受的一次性
232+
/// 迁移语义,登记观察项)。
233+
pub fn upgrade_existing_default_to_deepseek(store: &mut AiProviderStore) -> bool {
234+
let on_old_chain = store
235+
.default_provider_id
236+
.as_deref()
237+
.map(|id| id == "siliconflow" || id == "legacy-siliconflow")
238+
.unwrap_or(false);
239+
if !on_old_chain || store.get("deepseek").is_some() {
240+
return false;
241+
}
242+
let Some(ds) = preset_templates().into_iter().find(|p| p.id == "deepseek") else {
243+
return false;
244+
};
245+
store.providers.insert(0, ds);
246+
store.default_provider_id = Some("deepseek".to_string());
247+
true
248+
}
249+
221250
/// 单测独立文件(保持本文件 ≤300 行,AGENTS.md §3)。
222251
#[cfg(test)]
223252
#[path = "ai_provider_tests.rs"]

‎app/src-tauri/src/ai_provider_tests.rs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,57 @@ fn migrate_from_legacy_always_produces_valid_deepseek() {
110110
assert!(providers[0].validate().is_ok());
111111
assert_eq!(providers[0].default_model, "deepseek-v4-flash-vision-exp");
112112
assert_eq!(default_id, Some("legacy-deepseek".to_string()));
113+
}
114+
115+
/// v0.12.0 M4:既有安装默认链升级——旧 SiliconFlow 链默认 → DeepSeek 默认。
116+
#[test]
117+
fn upgrade_existing_siliconflow_default_to_deepseek() {
118+
let mut store = AiProviderStore::default();
119+
let sf = preset_templates().into_iter().find(|p| p.id == "siliconflow").unwrap();
120+
store.providers.push(sf);
121+
store.default_provider_id = Some("siliconflow".to_string());
122+
// Act
123+
assert!(upgrade_existing_default_to_deepseek(&mut store));
124+
// Assert:DeepSeek 插入首位并设默认;旧 Provider 保留(密钥/回退不受影响)
125+
assert_eq!(store.default_provider_id.as_deref(), Some("deepseek"));
126+
assert_eq!(store.providers[0].id, "deepseek");
127+
assert!(store.get("siliconflow").is_some(), "旧 Provider 必须保留");
128+
}
129+
130+
/// 已有 DeepSeek → 不重复插入(幂等——用户删除后不再误加由守卫承担)。
131+
#[test]
132+
fn upgrade_skips_when_deepseek_present() {
133+
let mut store = AiProviderStore::default();
134+
let ds = preset_templates().into_iter().find(|p| p.id == "deepseek").unwrap();
135+
let sf = preset_templates().into_iter().find(|p| p.id == "siliconflow").unwrap();
136+
store.providers.push(ds);
137+
store.providers.push(sf);
138+
store.default_provider_id = Some("siliconflow".to_string());
139+
let before = store.clone();
140+
// Act & Assert
141+
assert!(!upgrade_existing_default_to_deepseek(&mut store));
142+
assert_eq!(store, before, "已含 DeepSeek 时不得改动");
143+
}
144+
145+
/// 用户显式设其他默认(OpenRouter/自定义)→ 不覆盖用户选择。
146+
#[test]
147+
fn upgrade_skips_custom_default() {
148+
let mut store = AiProviderStore::default();
149+
let or = preset_templates().into_iter().find(|p| p.id == "openrouter").unwrap();
150+
store.providers.push(or);
151+
store.default_provider_id = Some("openrouter".to_string());
152+
// Act & Assert
153+
assert!(!upgrade_existing_default_to_deepseek(&mut store));
154+
assert_eq!(store.default_provider_id.as_deref(), Some("openrouter"));
155+
}
156+
157+
/// 默认未显式设置(None)→ 不触碰(尊重 first-enabled 语义)。
158+
#[test]
159+
fn upgrade_skips_unset_default() {
160+
let mut store = AiProviderStore::default();
161+
let sf = preset_templates().into_iter().find(|p| p.id == "siliconflow").unwrap();
162+
store.providers.push(sf);
163+
store.default_provider_id = None;
164+
// Act & Assert
165+
assert!(!upgrade_existing_default_to_deepseek(&mut store));
113166
}

‎app/src-tauri/src/app_setup.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
180180
}
181181
}
182182
store.save(&ai_providers_path)?;
183+
} else if crate::ai_provider::upgrade_existing_default_to_deepseek(&mut store) {
184+
// v0.12.0 M4:既有安装默认链升级(硅基旧链 → DeepSeek 默认;旧
185+
// Provider 保留不丢配置——真机取证:既有用户默认仍是 SiliconFlow)
186+
store.save(&ai_providers_path)?;
183187
}
184188
}
185189
// v0.8.0 M2(REQ-145):AI 异步任务注册表 + id 序列(spawn_blocking 后台

0 commit comments

Comments
 (0)