fix tray style activation and streamed text consistency

This commit is contained in:
cooper committed 2026-05-13 21:14:46 +08:00
1 parent 16dd99cf67
commit a4c4cc224b
5 files changed
+179 -75

No files matched your search

+51 -41
View File
@@ -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<StylePack, String> {
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<StylePack, String> {
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]
@@ -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<String>,
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<Inner>) {
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<Inner>) -> 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<Inner>) {
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");
}
}
+8 -7
View File
@@ -476,7 +476,12 @@ fn build_style_tray_menu<M: Manager<tauri::Wry>>(
app: &M,
coordinator: &Arc<coordinator::Coordinator>,
) -> tauri::Result<StyleTrayMenu> {
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::<Arc<coordinator::Coordinator>>();
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}");
}
@@ -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);
+19
View File
@@ -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,否则会把现有用户的粘贴
/// 行为静默改掉。