diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index 19b8e02e..54f4a194 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -200,7 +200,7 @@ fn emit_prefs_changed(app: &AppHandle, prefs: &UserPreferences) { let _ = app.emit_to("main", "prefs:changed", prefs); } -fn sync_style_pack_prefs_and_persist( +pub(crate) fn sync_style_pack_prefs_and_persist( coord: &Coordinator, app: &AppHandle, mut prefs: UserPreferences, @@ -216,6 +216,54 @@ fn sync_style_pack_prefs_and_persist( Ok(prefs) } +pub(crate) fn activate_style_pack_by_id( + coord: &Coordinator, + app: &AppHandle, + id: &str, +) -> Result { + let mut prefs = coord.prefs().get(); + let pack = coord.style_packs().get(id).map_err(|e| e.to_string())?; + log::info!( + "[style-pack] activate helper requested id={} kind={:?} base_mode={:?} enabled={}", + pack.id, + pack.kind, + pack.base_mode, + pack.enabled + ); + if !pack.enabled { + coord + .style_packs() + .set_enabled(id, true) + .map_err(|e| e.to_string())?; + } + prefs.active_style_pack_id = id.to_string(); + sync_style_pack_prefs_and_persist(coord, app, prefs)?; + log::info!("[style-pack] activate helper applied id={id}"); + coord + .style_packs() + .get(id) + .map(|mut pack| { + pack.active = true; + pack + }) + .map_err(|e| e.to_string()) +} + +pub(crate) fn activate_builtin_style_mode( + coord: &Coordinator, + app: &AppHandle, + mode: PolishMode, +) -> Result<(), String> { + let pack_id = builtin_style_pack_id(mode).to_string(); + log::info!( + "[style-pack] activate builtin mode helper mode={:?} pack_id={}", + mode, + pack_id + ); + let _ = activate_style_pack_by_id(coord, app, &pack_id)?; + Ok(()) +} + // ─────────────────────────── release channel (Beta opt-in) ─────────────────────────── // // 渠道偏好的写入路径跟 set_settings 复用 persist_settings:保持热键兜底归一化 @@ -1243,32 +1291,7 @@ pub fn set_active_style_pack( app: AppHandle, id: String, ) -> Result { - let mut prefs = coord.prefs().get(); - let pack = coord.style_packs().get(&id).map_err(|e| e.to_string())?; - log::info!( - "[style-pack] command activate requested id={} kind={:?} base_mode={:?} enabled={}", - pack.id, - pack.kind, - pack.base_mode, - pack.enabled - ); - if !pack.enabled { - coord - .style_packs() - .set_enabled(&id, true) - .map_err(|e| e.to_string())?; - } - prefs.active_style_pack_id = id.clone(); - sync_style_pack_prefs_and_persist(&*coord, &app, prefs)?; - log::info!("[style-pack] command activate applied id={id}"); - coord - .style_packs() - .get(&id) - .map(|mut pack| { - pack.active = true; - pack - }) - .map_err(|e| e.to_string()) + activate_style_pack_by_id(&coord, &app, &id) } #[tauri::command] @@ -1369,20 +1392,7 @@ pub fn set_default_polish_mode( app: AppHandle, mode: PolishMode, ) -> Result<(), String> { - let mut prefs = coord.prefs().get(); - let pack_id = builtin_style_pack_id(mode).to_string(); - log::info!( - "[style-pack] compat set_default_polish_mode mode={:?} pack_id={}", - mode, - pack_id - ); - coord - .style_packs() - .set_enabled(&pack_id, true) - .map_err(|e| e.to_string())?; - prefs.active_style_pack_id = pack_id; - let _ = sync_style_pack_prefs_and_persist(&*coord, &app, prefs)?; - Ok(()) + activate_builtin_style_mode(&coord, &app, mode) } #[tauri::command] diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index 1a8f770b..4376be9a 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -273,6 +273,44 @@ async fn run_streaming_polish( } } +fn finalize_polished_text( + polished: String, + translation_active: bool, + raw_uses_llm: bool, + mode: PolishMode, + polish_error: &Option, + chinese_script_preference: crate::types::ChineseScriptPreference, + correction_rules: &[crate::types::CorrectionRule], + already_streamed: bool, +) -> String { + if already_streamed { + return polished; + } + let should_force_script = if translation_active { + polish_error.is_some() + } else { + (!raw_uses_llm && mode == PolishMode::Raw) || polish_error.is_some() + }; + let polished = if should_force_script { + apply_chinese_script_preference(&polished, chinese_script_preference) + } else { + polished + }; + if correction_rules.is_empty() { + polished + } else { + let corrected = apply_correction_rules(&polished, correction_rules); + if corrected != polished { + log::info!( + "[coord] correction rules adjusted final text ({} → {} chars)", + polished.chars().count(), + corrected.chars().count() + ); + } + corrected + } +} + pub(super) async fn handle_pressed_edge(inner: &Arc) { let was_held = inner.hotkey_trigger_held.swap(true, Ordering::SeqCst); if !was_held { @@ -1334,32 +1372,16 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { (p, e, false) }; - // 仅在“ASR 直出文本”场景做强制简繁收敛,避免误伤成功的翻译/常规 LLM 输出: - // - 非翻译模式:mode=Raw(本来就不走润色)或润色失败回退 raw - // - 翻译模式:仅翻译失败回退 raw 时才收敛 - let should_force_script = if translation_active { - polish_error.is_some() - } else { - (!raw_uses_llm && mode == PolishMode::Raw) || polish_error.is_some() - }; - let polished = if should_force_script { - apply_chinese_script_preference(&polished, chinese_script_preference) - } else { - polished - }; - let polished = if correction_rules.is_empty() { - polished - } else { - let corrected = apply_correction_rules(&polished, &correction_rules); - if corrected != polished { - log::info!( - "[coord] correction rules adjusted final text ({} → {} chars)", - polished.chars().count(), - corrected.chars().count() - ); - } - corrected - }; + let polished = finalize_polished_text( + polished, + translation_active, + raw_uses_llm, + mode, + &polish_error, + chinese_script_preference, + &correction_rules, + already_streamed, + ); // 原子化最后一次 cancel 检查 + 转 Inserting: // 在同一 lock 内决定「丢弃」还是「进入 Inserting」。一旦设到 Inserting, // cancel_session 就拒绝介入(Cmd+V 已发出,撤销不掉)。这是 audit HIGH #2 的修复, @@ -1570,3 +1592,55 @@ pub(super) fn cancel_session(inner: &Arc) { log::info!("[coord] session cancelled (was {:?})", decision.phase); schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS); } + +#[cfg(test)] +mod tests { + use super::finalize_polished_text; + use crate::types::{ChineseScriptPreference, CorrectionRule, PolishMode}; + + fn correction_rule(pattern: &str, replacement: &str) -> CorrectionRule { + CorrectionRule { + id: "test".into(), + pattern: pattern.into(), + replacement: replacement.into(), + enabled: true, + created_at: String::new(), + } + } + + #[test] + fn streamed_output_skips_postprocessing_mutations() { + let rules = vec![correction_rule("Open AI", "OpenAI")]; + + let result = finalize_polished_text( + "Open AI".into(), + false, + false, + PolishMode::Raw, + &None, + ChineseScriptPreference::Auto, + &rules, + true, + ); + + assert_eq!(result, "Open AI"); + } + + #[test] + fn non_streamed_output_still_applies_correction_rules() { + let rules = vec![correction_rule("Open AI", "OpenAI")]; + + let result = finalize_polished_text( + "Open AI".into(), + false, + false, + PolishMode::Raw, + &None, + ChineseScriptPreference::Auto, + &rules, + false, + ); + + assert_eq!(result, "OpenAI"); + } +} diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 53749b38..420c4ba8 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -476,7 +476,12 @@ fn build_style_tray_menu>( app: &M, coordinator: &Arc, ) -> tauri::Result { - let selected = coordinator.prefs().get().default_mode; + let prefs = coordinator.prefs().get(); + let selected = coordinator + .style_packs() + .get_or_default_active(&prefs.active_style_pack_id) + .map(|pack| pack.base_mode) + .unwrap_or(prefs.default_mode); let mut submenu = SubmenuBuilder::with_id(app, "style", "输出风格"); for entry in tray_polish_mode_menu_entries(selected) { let item = CheckMenuItemBuilder::with_id(&entry.id, entry.label) @@ -630,14 +635,10 @@ fn handle_style_tray_menu_event(app: &AppHandle, id: &str) -> bool { return false; }; let coord = app.state::>(); - let mut prefs = coord.prefs().get(); - prefs.default_mode = mode; - if let Err(err) = coord.prefs().set(prefs.clone()) { - log::warn!("[tray] save polish mode preference failed: {err}"); + if let Err(err) = commands::activate_builtin_style_mode(&coord, app, mode) { + log::warn!("[tray] activate builtin style mode failed: {err}"); return true; } - let _ = app.emit("prefs:changed", &prefs); - let _ = app.emit_to("main", "prefs:changed", &prefs); if let Err(err) = refresh_tray_microphone_menu(app) { log::warn!("[tray] refresh style menu after polish mode change failed: {err}"); } diff --git a/openless-all/app/src-tauri/src/persistence.rs b/openless-all/app/src-tauri/src/persistence.rs index 1041173b..4e5db7e3 100644 --- a/openless-all/app/src-tauri/src/persistence.rs +++ b/openless-all/app/src-tauri/src/persistence.rs @@ -1012,7 +1012,7 @@ impl StylePackStore { } if let Some(pack) = packs .iter() - .find(|pack| pack.id == BUILTIN_STYLE_PACK_LIGHT_ID) + .find(|pack| pack.id == BUILTIN_STYLE_PACK_LIGHT_ID && pack.enabled) .cloned() { return Ok(pack); diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index d07f1f02..187fcb41 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -195,6 +195,7 @@ impl StyleSystemPrompts { } pub fn with_legacy_custom_prompts(mut self, legacy: &CustomStylePrompts) -> Self { + const LEGACY_CUSTOM_PROMPT_MARKER: &str = "\n\n# 用户自定义附加要求\n"; for mode in [ PolishMode::Raw, PolishMode::Light, @@ -205,6 +206,9 @@ impl StyleSystemPrompts { if legacy_prompt.is_empty() { continue; } + if self.for_mode(mode).contains(LEGACY_CUSTOM_PROMPT_MARKER) { + continue; + } let merged = format!( "{}\n\n# 用户自定义附加要求\n{}", self.for_mode(mode).trim_end(), @@ -1594,6 +1598,21 @@ mod tests { assert!(prefs.custom_style_prompts.has_for_mode(PolishMode::Formal)); } + #[test] + fn legacy_custom_style_prompts_are_not_appended_twice() { + let base = StyleSystemPrompts::default(); + let legacy = CustomStylePrompts { + light: "更像微信消息".into(), + ..CustomStylePrompts::default() + }; + + let once = base.clone().with_legacy_custom_prompts(&legacy); + let twice = once.clone().with_legacy_custom_prompts(&legacy); + + assert_eq!(once.light, twice.light); + assert_eq!(twice.light.matches("# 用户自定义附加要求").count(), 1); + } + /// issue #360: 默认值必须是 CtrlV,跟历史行为一致;老配置文件没有 /// pasteShortcut 字段时反序列化也得回到 CtrlV,否则会把现有用户的粘贴 /// 行为静默改掉。