Files
mixvideo-v2/apps/desktop/src-tauri/src/presentation/commands/tools_commands.rs
imeepos 7fb1dfa95a feat: 添加便捷小工具页面 - AI检索图片/数据清洗功能
- 新增便捷工具页面 (/tools),提供AI检索图片/数据清洗功能
- 支持JSONL格式数据的URI匹配去重处理
- 实现实时进度显示和批量数据处理
- 添加完整的错误处理和用户反馈机制
- 遵循Tauri开发规范和UI/UX设计标准
- 包含测试数据和功能文档

技术实现:
- 后端: Rust异步处理,流式文件读取,进度事件发送
- 前端: React + TypeScript,文件选择对话框,进度条显示
- 导航: 新增便捷工具菜单项,集成到主导航栏
2025-07-17 15:29:59 +08:00

223 lines
6.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use tauri::{command, Emitter};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataCleaningProgress {
pub current: usize,
pub total: usize,
pub percentage: f64,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataCleaningResult {
pub success: bool,
pub message: String,
pub original_count: usize,
pub removed_count: usize,
pub final_count: usize,
pub output_file: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StorageObject {
pub uri: String,
// 其他字段可以忽略,我们只关心 uri
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProcessedData {
pub json_data: String,
}
/// AI检索图片/数据清洗 - JSONL格式处理
#[command]
pub async fn clean_jsonl_data(
all_data_file: String,
remove_data_file: String,
output_file: String,
window: tauri::Window,
) -> Result<DataCleaningResult, String> {
// 验证文件存在
if !Path::new(&all_data_file).exists() {
return Err(format!("全部数据文件不存在: {}", all_data_file));
}
if !Path::new(&remove_data_file).exists() {
return Err(format!("要去除的数据文件不存在: {}", remove_data_file));
}
// 发送开始处理的进度
let _ = window.emit("data-cleaning-progress", DataCleaningProgress {
current: 0,
total: 0,
percentage: 0.0,
status: "开始处理...".to_string(),
});
// 第一步读取要去除的数据文件提取所有URI
let _ = window.emit("data-cleaning-progress", DataCleaningProgress {
current: 0,
total: 0,
percentage: 10.0,
status: "读取要去除的数据文件...".to_string(),
});
let remove_uris = match extract_remove_uris(&remove_data_file).await {
Ok(uris) => uris,
Err(e) => return Err(format!("读取要去除的数据文件失败: {}", e)),
};
let _ = window.emit("data-cleaning-progress", DataCleaningProgress {
current: 0,
total: 0,
percentage: 30.0,
status: format!("找到 {} 个要去除的URI", remove_uris.len()),
});
// 第二步:处理全部数据文件
let _ = window.emit("data-cleaning-progress", DataCleaningProgress {
current: 0,
total: 0,
percentage: 40.0,
status: "开始处理全部数据文件...".to_string(),
});
let result = match process_all_data_file(&all_data_file, &remove_uris, &output_file, &window).await {
Ok(result) => result,
Err(e) => return Err(format!("处理全部数据文件失败: {}", e)),
};
let _ = window.emit("data-cleaning-progress", DataCleaningProgress {
current: result.final_count,
total: result.original_count,
percentage: 100.0,
status: "处理完成".to_string(),
});
Ok(result)
}
/// 从要去除的数据文件中提取所有URI
async fn extract_remove_uris(file_path: &str) -> Result<HashSet<String>, String> {
let file = File::open(file_path)
.map_err(|e| format!("无法打开文件: {}", e))?;
let reader = BufReader::new(file);
let mut uris = HashSet::new();
for line in reader.lines() {
let line = line.map_err(|e| format!("读取行失败: {}", e))?;
if line.trim().is_empty() {
continue;
}
// 解析JSON
let processed_data: ProcessedData = serde_json::from_str(&line)
.map_err(|e| format!("解析JSON失败: {} - 行内容: {}", e, line))?;
// 从json_data字段中解析内部JSON
let inner_json: serde_json::Value = serde_json::from_str(&processed_data.json_data)
.map_err(|e| format!("解析内部JSON失败: {}", e))?;
// 提取uri字段
if let Some(uri) = inner_json.get("uri").and_then(|v| v.as_str()) {
uris.insert(uri.to_string());
}
}
Ok(uris)
}
/// 处理全部数据文件过滤掉要去除的URI
async fn process_all_data_file(
input_file: &str,
remove_uris: &HashSet<String>,
output_file: &str,
window: &tauri::Window,
) -> Result<DataCleaningResult, String> {
let input_file_handle = File::open(input_file)
.map_err(|e| format!("无法打开输入文件: {}", e))?;
let reader = BufReader::new(input_file_handle);
let mut output_file_handle = File::create(output_file)
.map_err(|e| format!("无法创建输出文件: {}", e))?;
let mut original_count = 0;
let mut removed_count = 0;
let mut final_count = 0;
// 首先计算总行数用于进度显示
let total_lines = count_lines(input_file)?;
for (line_number, line) in reader.lines().enumerate() {
let line = line.map_err(|e| format!("读取行失败: {}", e))?;
if line.trim().is_empty() {
continue;
}
original_count += 1;
// 解析JSON
let storage_object: StorageObject = match serde_json::from_str(&line) {
Ok(obj) => obj,
Err(e) => {
// 如果解析失败,跳过这一行但记录警告
eprintln!("警告: 解析JSON失败跳过行 {}: {} - 内容: {}", line_number + 1, e, line);
continue;
}
};
// 检查URI是否在要去除的列表中
if remove_uris.contains(&storage_object.uri) {
removed_count += 1;
} else {
// 写入输出文件
writeln!(output_file_handle, "{}", line)
.map_err(|e| format!("写入输出文件失败: {}", e))?;
final_count += 1;
}
// 每处理100行发送一次进度更新
if line_number % 100 == 0 {
let percentage = 40.0 + (line_number as f64 / total_lines as f64) * 50.0;
let _ = window.emit("data-cleaning-progress", DataCleaningProgress {
current: line_number,
total: total_lines,
percentage,
status: format!("已处理 {} 行,已去除 {} 个重复项", line_number, removed_count),
});
}
}
// 确保文件被正确写入
output_file_handle.flush()
.map_err(|e| format!("刷新输出文件失败: {}", e))?;
Ok(DataCleaningResult {
success: true,
message: "数据清洗完成".to_string(),
original_count,
removed_count,
final_count,
output_file: output_file.to_string(),
})
}
/// 计算文件行数
fn count_lines(file_path: &str) -> Result<usize, String> {
let file = File::open(file_path)
.map_err(|e| format!("无法打开文件计算行数: {}", e))?;
let reader = BufReader::new(file);
let count = reader.lines().count();
Ok(count)
}