diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index 54f4a194..dd714bba 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -28,6 +28,7 @@ use crate::types::{ builtin_style_pack_id, default_active_style_pack_id, ChineseScriptPreference, ComboBinding, CorrectionRule, CredentialsStatus, DictationSession, DictionaryEntry, HotkeyCapability, HotkeyStatus, OutputLanguagePreference, PolishMode, ShortcutBinding, StylePack, + StylePackKind, StylePackRuntimeDiagnostics, StyleSystemPrompts, UpdateChannel, UserPreferences, VocabPresetStore, WindowsImeStatus, }; @@ -157,8 +158,10 @@ pub fn set_settings( coord: CoordinatorState<'_>, app: AppHandle, tray_microphones: State<'_, TrayMicrophoneMenuState>, - prefs: UserPreferences, + mut prefs: UserPreferences, ) -> Result<(), String> { + let packs = coord.style_packs().list().map_err(|e| e.to_string())?; + sync_style_pack_preferences(&mut prefs, &packs); // 广播给所有 webview。issue #205:QaPanel 跑在独立 webview, // 没有 HotkeySettingsContext,必须靠事件感知录音键变化,否则面板可见时 // 用户改键会让浮窗里的 "{recordHotkey}" 文案一直停留在旧值。 @@ -1257,6 +1260,7 @@ pub fn list_style_packs(coord: CoordinatorState<'_>) -> Result, S #[tauri::command] pub fn save_style_pack( coord: CoordinatorState<'_>, + app: AppHandle, style_pack: StylePack, ) -> Result { log::info!( @@ -1265,10 +1269,15 @@ pub fn save_style_pack( style_pack.kind, style_pack.base_mode ); - coord + let saved = coord .style_packs() .upsert(style_pack) - .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; + if saved.kind == StylePackKind::Builtin { + let prefs = coord.prefs().get(); + let _ = sync_style_pack_prefs_and_persist(&*coord, &app, prefs)?; + } + Ok(saved) } #[tauri::command] @@ -1324,13 +1333,17 @@ pub fn set_style_pack_enabled( #[tauri::command] pub fn reset_builtin_style_pack( coord: CoordinatorState<'_>, + app: AppHandle, id: String, ) -> Result { log::info!("[style-pack] command reset_builtin requested id={id}"); - coord + let saved = coord .style_packs() .reset_builtin(&id) - .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; + let prefs = coord.prefs().get(); + let _ = sync_style_pack_prefs_and_persist(&*coord, &app, prefs)?; + Ok(saved) } #[tauri::command] diff --git a/openless-all/app/src-tauri/src/persistence.rs b/openless-all/app/src-tauri/src/persistence.rs index 4e5db7e3..c643d0a9 100644 --- a/openless-all/app/src-tauri/src/persistence.rs +++ b/openless-all/app/src-tauri/src/persistence.rs @@ -24,8 +24,9 @@ use uuid::Uuid; use crate::types::{ builtin_style_pack_for_mode, builtin_style_pack_id, builtin_style_packs, - default_active_style_pack_id, CorrectionRule, DictationSession, DictionaryEntry, PolishMode, - StylePack, StylePackExample, StylePackKind, UserPreferences, VocabPresetStore, + default_active_style_pack_id, CorrectionRule, CustomStylePrompts, DictationSession, + DictionaryEntry, PolishMode, StylePack, StylePackExample, StylePackKind, UserPreferences, + VocabPresetStore, BUILTIN_STYLE_PACK_LIGHT_ID, }; @@ -1407,6 +1408,10 @@ pub fn sync_style_pack_preferences(prefs: &mut UserPreferences, packs: &[StylePa changed = true; } + if sync_builtin_style_prompt_preferences(prefs, packs) { + changed = true; + } + if changed { log::info!( "[style-pack] sync_prefs active:{}->{} default_mode:{:?}->{:?} enabled_modes:{:?}->{:?}", @@ -1422,6 +1427,44 @@ pub fn sync_style_pack_preferences(prefs: &mut UserPreferences, packs: &[StylePa changed } +fn sync_builtin_style_prompt_preferences(prefs: &mut UserPreferences, packs: &[StylePack]) -> bool { + let mut changed = false; + let mut saw_builtin = false; + for mode in [ + PolishMode::Raw, + PolishMode::Light, + PolishMode::Structured, + PolishMode::Formal, + ] { + let Some(pack) = packs + .iter() + .find(|pack| pack.kind == StylePackKind::Builtin && pack.base_mode == mode) + else { + continue; + }; + saw_builtin = true; + let next_prompt = pack.prompt.clone(); + let current_prompt = prefs.style_system_prompts.for_mode(mode); + if current_prompt == next_prompt { + continue; + } + match mode { + PolishMode::Raw => prefs.style_system_prompts.raw = next_prompt, + PolishMode::Light => prefs.style_system_prompts.light = next_prompt, + PolishMode::Structured => prefs.style_system_prompts.structured = next_prompt, + PolishMode::Formal => prefs.style_system_prompts.formal = next_prompt, + } + changed = true; + } + + if saw_builtin && prefs.custom_style_prompts != CustomStylePrompts::default() { + prefs.custom_style_prompts = CustomStylePrompts::default(); + changed = true; + } + + changed +} + pub fn enabled_modes_from_style_packs(packs: &[StylePack]) -> Vec { let mut modes = Vec::new(); for mode in [ @@ -2013,10 +2056,10 @@ impl CredentialsVault { #[cfg(test)] mod tests { use super::{ - chunk_json_payload, list_vocab_presets, save_vocab_presets, + chunk_json_payload, list_vocab_presets, save_vocab_presets, sync_style_pack_preferences, validate_correction_rule_syntax, KEYRING_CHUNK_MAX_UTF16_UNITS, }; - use crate::types::{VocabPreset, VocabPresetStore}; + use crate::types::{builtin_style_packs, CustomStylePrompts, VocabPreset, VocabPresetStore}; use std::fs; use std::path::PathBuf; @@ -2075,4 +2118,36 @@ mod tests { assert!(validate_correction_rule_syntax("{num}到{num}粒", "{num}例").is_err()); assert!(validate_correction_rule_syntax("几粒", "{num}例").is_err()); } + + #[test] + fn sync_style_pack_preferences_uses_builtin_store_prompts_as_source_of_truth() { + let mut prefs = crate::types::UserPreferences { + style_system_prompts: crate::types::StyleSystemPrompts { + raw: "stale raw".into(), + light: "stale light".into(), + structured: "stale structured".into(), + formal: "stale formal".into(), + }, + custom_style_prompts: CustomStylePrompts { + raw: String::new(), + light: "legacy extra instruction".into(), + structured: String::new(), + formal: String::new(), + }, + ..Default::default() + }; + let mut packs = builtin_style_packs(); + let light = packs + .iter_mut() + .find(|pack| pack.id == "builtin.light") + .expect("builtin light pack"); + light.prompt = "fresh light prompt from store".into(); + + assert!(sync_style_pack_preferences(&mut prefs, &packs)); + assert_eq!(prefs.style_system_prompts.raw, packs[0].prompt); + assert_eq!(prefs.style_system_prompts.light, "fresh light prompt from store"); + assert_eq!(prefs.style_system_prompts.structured, packs[2].prompt); + assert_eq!(prefs.style_system_prompts.formal, packs[3].prompt); + assert_eq!(prefs.custom_style_prompts, CustomStylePrompts::default()); + } }