Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion openless-all/app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ serde_json = "1"
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] }
futures-util = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls", "native-tls", "stream"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls", "native-tls", "stream", "system-proxy"] }
zip = "2"
thiserror = "1"
anyhow = "1"
Expand Down
4 changes: 3 additions & 1 deletion openless-all/app/src-tauri/src/asr/local/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,9 @@ pub fn partial_actual_size(partial: &Path) -> u64 {
let mut seen: HashSet<usize> = HashSet::new();
let mut total: u64 = 0;
for line in content.lines() {
let Ok(idx) = line.trim().parse::<usize>() else { continue };
let Ok(idx) = line.trim().parse::<usize>() else {
continue;
};
if !seen.insert(idx) {
continue;
}
Expand Down
4 changes: 2 additions & 2 deletions openless-all/app/src-tauri/src/asr/volcengine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub struct VolcengineCredentials {

impl VolcengineCredentials {
pub fn default_resource_id() -> &'static str {
"volc.seedasr.sauc.duration"
"volc.bigasr.sauc.duration"
}
}

Expand Down Expand Up @@ -719,7 +719,7 @@ mod tests {
fn default_resource_id_is_sauc_duration() {
assert_eq!(
VolcengineCredentials::default_resource_id(),
"volc.seedasr.sauc.duration"
"volc.bigasr.sauc.duration"
);
}

Expand Down
12 changes: 5 additions & 7 deletions openless-all/app/src-tauri/src/asr/whisper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ pub struct WhisperBatchASR {
}

impl WhisperBatchASR {
pub fn new(
api_key: String,
base_url: String,
model: String,
prompt: Option<String>,
) -> Self {
pub fn new(api_key: String, base_url: String, model: String, prompt: Option<String>) -> Self {
Self {
api_key,
base_url,
Expand Down Expand Up @@ -205,7 +200,10 @@ mod tests {
#[test]
fn build_prompt_single_phrase() {
let phrases = vec!["梁山泊".to_string()];
assert_eq!(build_prompt_from_phrases(&phrases), Some("梁山泊.".to_string()));
assert_eq!(
build_prompt_from_phrases(&phrases),
Some("梁山泊.".to_string())
);
}

#[test]
Expand Down
77 changes: 66 additions & 11 deletions openless-all/app/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! Tauri command surface — every IPC entry the React UI invokes lives here.

use std::sync::Arc;
use std::time::Duration;

use parking_lot::Mutex;
use serde::Serialize;
Expand All @@ -16,7 +15,11 @@ use crate::asr::local::FoundryLocalRuntime;
use crate::coordinator::Coordinator;
use crate::permissions::{self, PermissionStatus};
use crate::persistence::{CredentialAccount, CredentialsSnapshot, CredentialsVault};
use crate::polish::{LLMError, OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
use crate::polish::{
http_client_builder, CodexOAuthConfig, CodexOAuthCredentials, CodexOAuthLLMProvider, LLMError,
OpenAICompatibleConfig, OpenAICompatibleLLMProvider, CODEX_DEFAULT_MODEL,
CODEX_OAUTH_PROVIDER_ID,
};
use crate::recorder::{AudioConsumer, Recorder};
use crate::types::{
ChineseScriptPreference, ComboBinding, CorrectionRule, CredentialsStatus, DictationSession,
Expand Down Expand Up @@ -392,6 +395,9 @@ fn asr_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bo
}

fn llm_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bool {
if provider == CODEX_OAUTH_PROVIDER_ID {
return CodexOAuthCredentials::load_default().is_ok();
}
let endpoint = snap.ark_endpoint.as_deref().unwrap_or_default();
let endpoint_and_model = configured(&snap.ark_endpoint) && configured(&snap.ark_model_id);
if endpoint_and_model
Expand Down Expand Up @@ -554,6 +560,16 @@ pub async fn list_provider_models(kind: String) -> Result<ProviderModelsResult,
models: vec![crate::asr::bailian::DEFAULT_MODEL.to_string()],
});
}
if kind == "llm" && CredentialsVault::get_active_llm() == CODEX_OAUTH_PROVIDER_ID {
return Ok(ProviderModelsResult {
models: vec![
CODEX_DEFAULT_MODEL.to_string(),
"gpt-5.3-codex".to_string(),
"gpt-5.4".to_string(),
"gpt-5.5".to_string(),
],
});
}
let config = read_openai_provider_config(&kind)?;
fetch_provider_models(&config)
.await
Expand Down Expand Up @@ -595,6 +611,33 @@ fn read_openai_provider_config(kind: &str) -> Result<ProviderConfig, String> {
}

async fn validate_llm_provider() -> Result<(), String> {
if CredentialsVault::get_active_llm() == CODEX_OAUTH_PROVIDER_ID {
let model = CredentialsVault::get(CredentialAccount::ArkModelId)
.map_err(|e| e.to_string())?
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| CODEX_DEFAULT_MODEL.to_string());
let provider = CodexOAuthLLMProvider::new(CodexOAuthConfig::new(model));
return provider
.polish(
"验证连接",
PolishMode::Raw,
&[],
&[],
ChineseScriptPreference::Auto,
OutputLanguagePreference::Auto,
None,
&[],
)
.await
.map(|_| ())
.map_err(|e| match e {
LLMError::InvalidResponse { status, .. } => {
format!("providerHttpStatus:{status}")
}
other => other.to_string(),
});
}

let config = read_openai_provider_config("llm")?;
let model = CredentialsVault::get(CredentialAccount::ArkModelId)
.map_err(|e| e.to_string())?
Expand Down Expand Up @@ -711,8 +754,7 @@ async fn validate_asr_transcription(config: &ProviderConfig, model: &str) -> Res
let form = reqwest::multipart::Form::new()
.part("file", wav_part)
.text("model", model.to_string());
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(20))
let client = http_client_builder(&url, 20)
.build()
.map_err(|_| "providerClientInitFailed".to_string())?;
let response = client
Expand Down Expand Up @@ -812,8 +854,7 @@ async fn fetch_provider_models(config: &ProviderConfig) -> Result<Vec<String>, S
let url = models_url(&config.base_url);
let is_gemini = is_gemini_base_url(&config.base_url);
log::info!("[provider-check] GET {url} (gemini={is_gemini})");
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
let client = http_client_builder(&config.base_url, 15)
.build()
.map_err(|e| format!("HTTP client 初始化失败: {e}"))?;
let mut request = client.get(&url);
Expand Down Expand Up @@ -903,15 +944,23 @@ fn parse_gemini_model_ids(body: &str) -> Result<Vec<String>, String> {
let mut ids = models
.iter()
.filter(|item| {
match item.get("supportedGenerationMethods").and_then(|v| v.as_array()) {
match item
.get("supportedGenerationMethods")
.and_then(|v| v.as_array())
{
Some(methods) => methods
.iter()
.any(|m| m.as_str() == Some("generateContent")),
None => true, // 字段缺失:保守包含
}
})
.filter_map(|item| item.get("name").and_then(|n| n.as_str()))
.map(|name| name.strip_prefix("models/").unwrap_or(name).trim().to_string())
.map(|name| {
name.strip_prefix("models/")
.unwrap_or(name)
.trim()
.to_string()
})
.filter(|id| !id.is_empty())
.collect::<Vec<_>>();
ids.sort();
Expand Down Expand Up @@ -2234,10 +2283,16 @@ mod tests {

#[test]
fn is_gemini_base_url_matches_official_domain() {
assert!(is_gemini_base_url("https://generativelanguage.googleapis.com/v1beta"));
assert!(is_gemini_base_url("https://generativelanguage.googleapis.com/v1beta/"));
assert!(is_gemini_base_url(
"https://generativelanguage.googleapis.com/v1beta"
));
assert!(is_gemini_base_url(
"https://generativelanguage.googleapis.com/v1beta/"
));
assert!(!is_gemini_base_url("https://api.openai.com/v1"));
assert!(!is_gemini_base_url("https://ark.cn-beijing.volces.com/api/v3"));
assert!(!is_gemini_base_url(
"https://ark.cn-beijing.volces.com/api/v3"
));
}

#[test]
Expand Down
66 changes: 30 additions & 36 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ use crate::persistence::{
};

use crate::llm_gemini::{GeminiConfig, GeminiProvider};
use crate::polish::{OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
use crate::polish::{
ActiveLLMProvider, CodexOAuthConfig, CodexOAuthLLMProvider, OpenAICompatibleConfig,
OpenAICompatibleLLMProvider, CODEX_DEFAULT_MODEL, CODEX_OAUTH_PROVIDER_ID,
};
use crate::qa_hotkey::{QaHotkeyError, QaHotkeyEvent, QaHotkeyMonitor};
use crate::recorder::{Recorder, RecorderError};
use crate::selection::capture_selection;
Expand Down Expand Up @@ -2068,18 +2071,7 @@ async fn polish_text(
.await?);
}

let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "deepseek-v3-2".to_string());
let endpoint = resolve_ark_endpoint(&api_key)?;
let base_url = endpoint
.trim_end_matches("/chat/completions")
.trim_end_matches('/')
.to_string();

let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model);
let provider = OpenAICompatibleLLMProvider::new(config);
let provider = build_active_llm_provider()?;
Ok(provider
.polish(
raw,
Expand Down Expand Up @@ -2146,18 +2138,7 @@ async fn translate_text(
.await?);
}

let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "deepseek-v3-2".to_string());
let endpoint = resolve_ark_endpoint(&api_key)?;
let base_url = endpoint
.trim_end_matches("/chat/completions")
.trim_end_matches('/')
.to_string();

let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model);
let provider = OpenAICompatibleLLMProvider::new(config);
let provider = build_active_llm_provider()?;
Ok(provider
.translate_to(
raw,
Expand Down Expand Up @@ -2736,17 +2717,7 @@ where
.await?);
}

let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "deepseek-v3-2".to_string());
let endpoint = resolve_ark_endpoint(&api_key)?;
let base_url = endpoint
.trim_end_matches("/chat/completions")
.trim_end_matches('/')
.to_string();
let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model);
let provider = OpenAICompatibleLLMProvider::new(config);
let provider = build_active_llm_provider()?;
Ok(provider
.answer_chat_streaming(
messages,
Expand Down Expand Up @@ -2784,6 +2755,29 @@ fn read_gemini_credentials() -> anyhow::Result<(String, String, String)> {
Ok((api_key, model, base_url))
}

fn build_active_llm_provider() -> anyhow::Result<ActiveLLMProvider> {
let active = CredentialsVault::get_active_llm();
let model =
CredentialsVault::get(CredentialAccount::ArkModelId)?.filter(|s| !s.trim().is_empty());
if active == CODEX_OAUTH_PROVIDER_ID {
let config =
CodexOAuthConfig::new(model.unwrap_or_else(|| CODEX_DEFAULT_MODEL.to_string()));
return Ok(ActiveLLMProvider::Codex(CodexOAuthLLMProvider::new(config)));
}

let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
let model = model.unwrap_or_else(|| "deepseek-v3-2".to_string());
let endpoint = resolve_ark_endpoint(&api_key)?;
let base_url = endpoint
.trim_end_matches("/chat/completions")
.trim_end_matches('/')
.to_string();
let config = OpenAICompatibleConfig::new(active, "OpenLess LLM", base_url, api_key, model);
Ok(ActiveLLMProvider::OpenAI(OpenAICompatibleLLMProvider::new(
config,
)))
}

fn resolve_ark_endpoint(api_key: &str) -> anyhow::Result<String> {
let endpoint = CredentialsVault::get(CredentialAccount::ArkEndpoint)?.filter(|s| !s.is_empty());
resolve_ark_endpoint_with_policy(api_key, endpoint)
Expand Down
3 changes: 1 addition & 2 deletions openless-all/app/src-tauri/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,8 +796,7 @@ impl HistoryStore {
// Prepend so the newest session is at index 0, matching the Swift impl.
sessions.insert(0, session);
if retention_days > 0 {
let cutoff =
chrono::Utc::now() - chrono::Duration::days(i64::from(retention_days));
let cutoff = chrono::Utc::now() - chrono::Duration::days(i64::from(retention_days));
sessions.retain(|s| {
chrono::DateTime::parse_from_rfc3339(&s.created_at)
.map(|t| t.with_timezone(&chrono::Utc) >= cutoff)
Expand Down
Loading
Loading