Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,20 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
return Ok(());
}

if let Err(message) = ensure_asr_credentials() {
log::warn!("[coord] ASR credential gate failed: {message}");
emit_capsule(
inner,
CapsuleState::Error,
0.0,
0,
Some(message.clone()),
None,
);
inner.state.lock().phase = SessionPhase::Idle;
return Err(message);
}

if let Err(message) = ensure_microphone_permission(inner) {
log::warn!("[coord] microphone permission gate failed: {message}");
emit_capsule(
Expand Down Expand Up @@ -556,12 +570,45 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
};

// ASR 完成后 cancel 检查:用户在 transcribe 进行中按 Esc 时,这里就会命中。
// 优先级高于 empty 检查 — 用户取消 → 静默丢弃,不写失败历史也不弹错误胶囊。
if inner.state.lock().cancelled {
log::info!("[coord] cancel detected after ASR — discarding transcript");
inner.state.lock().phase = SessionPhase::Idle;
return Ok(());
}

// ASR 返回空转写护栏(来自 PR #66):写一条 emptyTranscript 失败历史 + 错误胶囊,
// 与 main 上其它 error 路径保持一致(带 schedule_capsule_idle 让胶囊自动消失)。
if raw.text.trim().is_empty() {
let session = DictationSession {
id: Uuid::new_v4().to_string(),
created_at: Utc::now().to_rfc3339(),
raw_transcript: raw.text.clone(),
final_text: String::new(),
mode: inner.prefs.get().default_mode,
app_bundle_id: None,
app_name: None,
insert_status: InsertStatus::Failed,
error_code: Some("emptyTranscript".to_string()),
duration_ms: Some(raw.duration_ms),
dictionary_entry_count: Some(enabled_phrases(inner).len() as u32),
};
if let Err(e) = inner.history.append(session) {
log::error!("[coord] history append failed: {e}");
}
emit_capsule(
inner,
CapsuleState::Error,
0.0,
elapsed,
Some("ASR returned empty transcript".to_string()),
None,
);
inner.state.lock().phase = SessionPhase::Idle;
schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS);
return Err("ASR returned empty transcript".to_string());
}

emit_capsule(inner, CapsuleState::Polishing, 0.0, elapsed, None, None);

let prefs = inner.prefs.get();
Expand Down Expand Up @@ -720,6 +767,27 @@ fn ensure_microphone_permission(inner: &Arc<Inner>) -> Result<(), String> {
}
}

fn ensure_asr_credentials() -> Result<(), String> {
let active_asr = CredentialsVault::get_active_asr();
if active_asr == "whisper" {
let api_key = CredentialsVault::get(CredentialAccount::AsrApiKey)
.ok()
.flatten()
.unwrap_or_default();
if api_key.trim().is_empty() {
return Err("请先在设置中填写 Whisper ASR API Key".to_string());
}
return Ok(());
}

let creds = read_volc_credentials();
if creds.app_id.trim().is_empty() || creds.access_token.trim().is_empty() {
Err("请先在设置中填写火山引擎 ASR App Key 和 Access Key".to_string())
} else {
Ok(())
}
}

async fn polish_or_passthrough(
raw: &RawTranscript,
mode: PolishMode,
Expand Down
Loading