This commit is contained in:
949036910@qq.com
2026-05-30 11:52:25 +08:00
parent 30e019d345
commit 58240c3d76
9 changed files with 265 additions and 16 deletions

View File

@@ -16,10 +16,15 @@ const APP_COMMANDS: &[&str] = &[
"run_nodejs_script_source",
"list_nodejs_scripts",
"read_local_file_base64",
"write_local_file_base64",
"get_media_duration_seconds",
"copy_local_file",
"write_local_text_file",
"temp_voice_reference_path",
"stage_voice_reference_audio",
"import_voice_clone_reference",
"reveal_local_file_in_folder",
"stop_nodejs_script",
"list_avatars",
"insert_avatar",
"update_avatar_name",

View File

@@ -18,10 +18,15 @@ permissions = [
"allow-run-nodejs-script-source",
"allow-list-nodejs-scripts",
"allow-read-local-file-base64",
"allow-write-local-file-base64",
"allow-get-media-duration-seconds",
"allow-copy-local-file",
"allow-write-local-text-file",
"allow-temp-voice-reference-path",
"allow-stage-voice-reference-audio",
"allow-import-voice-clone-reference",
"allow-reveal-local-file-in-folder",
"allow-stop-nodejs-script",
"allow-list-avatars",
"allow-insert-avatar",
"allow-update-avatar-name",

View File

@@ -14,6 +14,46 @@ fn normalize_path(path: &str) -> PathBuf {
.collect()
}
/// 解析用户通过系统对话框选择的文件路径(兼容 file:// 与 /C:/ 形式)。
fn resolve_user_selected_file(path: &str) -> Result<PathBuf, String> {
let mut trimmed = path.trim();
if trimmed.is_empty() {
return Err("路径为空".into());
}
if let Some(rest) = trimmed.strip_prefix("file:///") {
trimmed = rest;
} else if let Some(rest) = trimmed.strip_prefix("file://") {
trimmed = rest;
}
// `/C:/Users/...` → `C:/Users/...`
if trimmed.len() >= 3 {
let bytes = trimmed.as_bytes();
if bytes[0] == b'/' && bytes[2] == b':' {
trimmed = &trimmed[1..];
}
}
let direct = PathBuf::from(trimmed);
if direct.is_file() {
return Ok(direct);
}
let normalized = normalize_path(trimmed);
if normalized.is_file() {
return Ok(normalized);
}
if let Ok(canonical) = dunce::canonicalize(&direct) {
if canonical.is_file() {
return Ok(canonical);
}
}
if let Ok(canonical) = dunce::canonicalize(&normalized) {
if canonical.is_file() {
return Ok(canonical);
}
}
Err(format!("文件不存在: {trimmed}"))
}
/// 仅允许读取临时目录下的试听/流水线产物,避免任意路径读取。
fn is_allowed_local_media(path: &Path) -> bool {
let lower = path.to_string_lossy().to_lowercase();
@@ -165,6 +205,34 @@ fn voice_clones_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
Ok(dir)
}
/// 将用户通过文件对话框选择的音频复制到临时目录,供时长探测与试听。
#[tauri::command]
pub async fn stage_voice_reference_audio(source_path: String) -> Result<String, String> {
let source = resolve_user_selected_file(&source_path)?;
let ext = source
.extension()
.and_then(|e| e.to_str())
.unwrap_or("wav");
let ext = if ext.is_empty() { "wav" } else { ext };
let dir = std::env::temp_dir().join("aiclient-voice-ref");
tokio::fs::create_dir_all(&dir)
.await
.map_err(|e| format!("创建临时目录失败: {e}"))?;
let dest = dir.join(format!(
"upload_{}_{}.{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0),
rand_simple(),
ext
));
tokio::fs::copy(&source, &dest)
.await
.map_err(|e| format!("复制参考音频失败: {e}"))?;
Ok(dest.to_string_lossy().into_owned())
}
/// 生成临时参考音频路径(录音/上传中转)。
#[tauri::command]
pub async fn temp_voice_reference_path(ext: String) -> Result<String, String> {

View File

@@ -137,6 +137,7 @@ pub fn run() {
commands::fs_util::copy_local_file,
commands::fs_util::write_local_text_file,
commands::fs_util::temp_voice_reference_path,
commands::fs_util::stage_voice_reference_audio,
commands::fs_util::import_voice_clone_reference,
commands::fs_util::reveal_local_file_in_folder,
commands::avatar::list_avatars,