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
37 changes: 37 additions & 0 deletions openless-all/app/src-tauri/src/asr/local/test_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,43 @@ pub async fn run_test(model_id: ModelId) -> Result<TestResult> {
anyhow::bail!("模型目录不存在:{}(请先下载)", dir.display());
}

// ── 模型文件完整性检查 ────────────────────────────────────────────
// 在调 C FFI 之前先检查关键文件是否齐全、尺寸是否合理,避免因下载不完整
// 或文件损坏导致 C 端 qwen_load / qwen_transcribe_audio segfault 杀死进程。
// 需要的文件清单见 QwenAsrEngine::load() 注释。
let required_files = ["config.json", "vocab.json", "merges.txt"];
for fname in &required_files {
let path = dir.join(fname);
if !path.exists() {
anyhow::bail!("模型文件缺失:{fname},请重新下载(预期路径:{})", path.display());
}
let meta = std::fs::metadata(&path)
.map_err(|e| anyhow::anyhow!("读取 {fname} 元数据失败:{e}"))?;
if meta.len() == 0 {
anyhow::bail!("模型文件为空:{fname},请重新下载");
}
}
// safetensors 可能是单文件 model.safetensors 或分片 model-00001-of-NNNN.safetensors
let has_safetensors: Vec<_> = std::fs::read_dir(&dir)
.map_err(|e| anyhow::anyhow!("读取模型目录失败:{e}"))?
.filter_map(|entry| entry.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "safetensors"))
.collect();
if has_safetensors.is_empty() {
anyhow::bail!("模型目录中没有 .safetensors 权重文件,请重新下载");
}
for entry in &has_safetensors {
let meta = std::fs::metadata(entry.path())
.map_err(|e| anyhow::anyhow!("读取 {} 元数据失败:{e}", entry.path().display()))?;
if meta.len() < 1024 {
anyhow::bail!(
"权重文件太小({} bytes):{},请重新下载",
meta.len(),
entry.path().display()
);
}
}

let samples = decode_wav_16k_mono(TEST_WAV)?;
let audio_ms = (samples.len() as u64) * 1000 / 16_000;

Expand Down
60 changes: 53 additions & 7 deletions openless-all/app/src-tauri/src/coordinator/llm_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,19 @@ where
);
return StreamingPolishOutcome::UnsupportedFallback;
}
let provider = match build_active_llm_provider(llm_thinking_enabled) {
Ok(p) => p,
Err(e) => {
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_active_llm_provider(llm_thinking_enabled)
})) {
Ok(Ok(p)) => p,
Ok(Err(e)) => {
log::error!("[coord] streaming polish: build provider failed: {e}");
return StreamingPolishOutcome::Failed(e.to_string());
}
Err(panic) => {
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
log::error!("[coord] {msg}");
return StreamingPolishOutcome::Failed(msg);
}
};
if !provider.supports_streaming_polish() {
log::info!(
Expand Down Expand Up @@ -259,7 +266,17 @@ pub(crate) async fn polish_text(
.await?);
}

let provider = build_active_llm_provider(llm_thinking_enabled)?;
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_active_llm_provider(llm_thinking_enabled)
})) {
Ok(Ok(p)) => p,
Ok(Err(e)) => return Err(e),
Err(panic) => {
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
log::error!("[coord] {msg}");
anyhow::bail!(msg);
}
};
Ok(provider
.polish(
raw,
Expand Down Expand Up @@ -305,7 +322,17 @@ pub(crate) async fn translate_text(
.await?);
}

let provider = build_active_llm_provider(llm_thinking_enabled)?;
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_active_llm_provider(llm_thinking_enabled)
})) {
Ok(Ok(p)) => p,
Ok(Err(e)) => return Err(e),
Err(panic) => {
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
log::error!("[coord] {msg}");
anyhow::bail!(msg);
}
};
Ok(provider
.translate_to(
raw,
Expand Down Expand Up @@ -597,7 +624,17 @@ where
.await?);
}

let provider = build_active_llm_provider(llm_thinking_enabled)?;
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_active_llm_provider(llm_thinking_enabled)
})) {
Ok(Ok(p)) => p,
Ok(Err(e)) => return Err(e),
Err(panic) => {
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
log::error!("[coord] {msg}");
anyhow::bail!(msg);
}
};
Ok(provider
.answer_chat_streaming(
messages,
Expand Down Expand Up @@ -687,8 +724,17 @@ pub(crate) fn resolve_ark_endpoint_with_policy(
if api_key.trim().is_empty() && endpoint.is_none() {
anyhow::bail!("API Key 为空");
}
let resolved = endpoint
let mut resolved = endpoint
.unwrap_or_else(|| "https://ark.cn-beijing.volces.com/api/v3/chat/completions".to_string());
// 兜底:用户可能在自定义 endpoint 时只写了 "192.168.1.100:8080/v1" 漏了 scheme,
// 被 reqwest::IntoUrl 自动补 https:// 后对纯 HTTP 的 llama.cpp 发请求会 TLS 握手失败。
// 检测到无 scheme 时自动补 http://(局域网自托管服务最常见的形态)。
// 用 to_ascii_lowercase 防用户输大写如 HTTP://... 时被误判为缺 scheme。
let lower = resolved.to_ascii_lowercase();
if !lower.starts_with("http://") && !lower.starts_with("https://") {
resolved = format!("http://{resolved}");
log::info!("[llm] endpoint missing scheme, auto-prepended http://");
}
// issue #609 F-01(SSRF):用户自定义 endpoint 是 attacker-controlled,直接拿来发
// 带 API Key 的请求等于把凭据指哪打哪。这里对 host/IP 段 + scheme 做配置时校验,
// 默认官方 endpoint 也过一遍(它本就合法)。注意这只防住"配置即字面内网地址",
Expand Down
13 changes: 10 additions & 3 deletions openless-all/app/src-tauri/src/polish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1252,9 +1252,16 @@ async fn send_with_transient_retry(
request: reqwest::RequestBuilder,
) -> Result<reqwest::Response, LLMError> {
const RETRY_DELAY_MS: u64 = 500;
let initial = request
.try_clone()
.expect("memory-backed body (json/form) must be clonable for retry");
let Some(initial) = request.try_clone() else {
// try_clone 失败(如 stream body 不可 clone)→ 不走重试,直接 send 一次。
// 用 expect 会 panic 杀死整个进程,这里兜底为单次发送。
log::warn!("[llm] request body not clonable, skipping retry");
return match request.send().await {
Ok(r) => Ok(r),
Err(e) if e.is_timeout() => Err(LLMError::Timeout),
Err(e) => Err(LLMError::Network(e.to_string())),
};
};
match initial.send().await {
Ok(r) => Ok(r),
Err(e) if e.is_connect() || e.is_request() => {
Expand Down
Loading