fix: polish 失败有感知 + 词汇启停失败回滚 (closes #57 #60)

#57: polish 失败静默回退原文 → 用户感知'风格设置没生效'
- polish_or_passthrough 返回类型 String → (String, Option<String>)
  失败时第二个返回值是 reason,let _ = drop 用不到
- end_session: polish_error.is_some() 时 done_message='润色失败,已插入原文'
  + history.error_code='polishFailed'。文本仍插入(保留'用户的话不丢'语义)

#60: 词汇启停乐观更新无回滚
- Vocab.tsx::onToggle try { await setVocabEnabled } catch { setEntries 回滚 + 显示错误 }
  避免 UI 显示已禁用但 ASR/polish 仍注入
This commit is contained in:
baiqing committed 2026-04-30 17:55:23 +08:00
1 parent 1683224403
commit ef9db9645d
2 files changed
+35 -16

No files matched your search

+27 -15
View File
@@ -614,7 +614,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
let prefs = inner.prefs.get();
let mode = prefs.default_mode;
let hotword_strs = enabled_phrases(inner);
let polished = polish_or_passthrough(&raw, mode, &hotword_strs).await;
let (polished, polish_error) = polish_or_passthrough(&raw, mode, &hotword_strs).await;
// 原子化最后一次 cancel 检查 + 转 Inserting:
// 在同一 lock 内决定「丢弃」还是「进入 Inserting」。一旦设到 Inserting,
@@ -655,6 +655,10 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
}
}
// polish 失败时在 history 里标记 polishFailed,让用户能在历史详情看到为什么这次输出
// 不是预期的 mode 风格。即使失败也不丢词 — final_text 仍是原文(保留"用户的话不丢"语义)。
let error_code = polish_error.as_ref().map(|_| "polishFailed".to_string());
let session = DictationSession {
id: Uuid::new_v4().to_string(),
created_at: Utc::now().to_rfc3339(),
@@ -664,7 +668,7 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
app_bundle_id: None,
app_name: None,
insert_status: status,
error_code: None,
error_code,
duration_ms: Some(raw.duration_ms),
// 历史详情页的"X 个热词"显示:用本次实际命中次数(每个匹配实例算一次),
// 比"启用词条总数"更能反映本段口述命中了多少。u64 → u32 截断对单段听写足够。
@@ -674,14 +678,19 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
log::error!("[coord] history append failed: {e}");
}
let done_message = match status {
InsertStatus::Inserted => None,
InsertStatus::CopiedFallback => Some(if cfg!(target_os = "windows") {
"已复制,请 Ctrl+V".to_string()
} else {
"已复制,请粘贴".to_string()
}),
InsertStatus::Failed => Some("插入失败".to_string()),
let done_message = if polish_error.is_some() {
// polish 失败优先告知用户,即使 insert 成功也要让用户知道这版是原文
Some("润色失败,已插入原文".to_string())
} else {
match status {
InsertStatus::Inserted => None,
InsertStatus::CopiedFallback => Some(if cfg!(target_os = "windows") {
"已复制,请 Ctrl+V".to_string()
} else {
"已复制,请粘贴".to_string()
}),
InsertStatus::Failed => Some("插入失败".to_string()),
}
};
emit_capsule(
@@ -788,19 +797,22 @@ fn ensure_asr_credentials() -> Result<(), String> {
}
}
/// 润色文本;失败时返回原文 + 失败原因,调用方据此弹错误胶囊 + 写历史 error_code。
/// 之前固定返回 String,调用方拿不到失败信号 → 用户感知"为什么风格设置没生效"。issue #57。
async fn polish_or_passthrough(
raw: &RawTranscript,
mode: PolishMode,
hotwords: &[String],
) -> String {
) -> (String, Option<String>) {
if mode == PolishMode::Raw {
return raw.text.clone();
return (raw.text.clone(), None);
}
match polish_text(&raw.text, mode, hotwords).await {
Ok(s) => s,
Ok(s) => (s, None),
Err(e) => {
log::error!("[coord] polish failed, falling back to raw: {e}");
raw.text.clone()
let reason = e.to_string();
log::error!("[coord] polish failed, falling back to raw: {reason}");
(raw.text.clone(), Some(reason))
}
}
}
+8 -1
View File
@@ -71,8 +71,15 @@ export function Vocab() {
const onToggle = async (entry: DictionaryEntry) => {
const next = !entry.enabled;
// 乐观更新 UI;后端失败时回滚 + 让用户看到错误,避免 UI 显示「已禁用」但 ASR/polish
// 仍在注入此词条造成的诡异状态。issue #60。
setEntries(prev => prev.map(e => (e.id === entry.id ? { ...e, enabled: next } : e)));
await setVocabEnabled(entry.id, next);
try {
await setVocabEnabled(entry.id, next);
} catch (err) {
setEntries(prev => prev.map(e => (e.id === entry.id ? { ...e, enabled: entry.enabled } : e)));
setError(err instanceof Error ? err.message : String(err));
}
};
return (