From be726634f2e4285fcc45c170a41eee38dc620d4a Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:49:26 +0800 Subject: [PATCH 01/12] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📦 添加新依赖 - reqwest: 异步HTTP客户端 - serde_json: JSON序列化支持 - anyhow: 错误处理工具 Signed-off-by: Xch13 <149986830+Xch13@users.noreply.github.com> Date: 2025-02-13 19:40:00 --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 50a8b84..ad7a9c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,14 @@ edition = "2021" egui = "0.29.1" eframe = "0.29.1" dirs-next = "2.0" -tokio = { version = "1.42.0", features = ["full"] } walkdir = "2.4" +tokio = { version = "1.42.0", features = ["full"] } log = "0.4" simplelog = "0.12" sha2 = "0.10" native-dialog = "0.7.0" serde = { version = "1.0.216", features = ["derive"] } serde_yaml = "0.9.34+deprecated" +serde_json = "1.0" +reqwest = { version = "0.11", features = ["json"] } +anyhow = "1.0" \ No newline at end of file From a442e2dd116ffac4daa5f483aacca3170d205a05 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:49:32 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0AI?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 添加AI驱动配置生成模块 - 添加配置生成器实现 - 添加JSON验证和处理 - 添加配置模板 功能包括: - 异步配置生成 - 配置文件读写 - JSON序列化支持 - 单元测试 Signed-off-by: Xch13 <149986830+Xch13@users.noreply.github.com> Date: 2025-02-13 19:40:00 --- src/ai_config.rs | 309 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 src/ai_config.rs diff --git a/src/ai_config.rs b/src/ai_config.rs new file mode 100644 index 0000000..804aee5 --- /dev/null +++ b/src/ai_config.rs @@ -0,0 +1,309 @@ +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +use serde::{Deserialize, Serialize}; +use serde_yaml; +use std::{collections::HashMap, env, fs, path::Path, thread, time::Duration}; +use tokio; +use anyhow::{Result, Context}; + +#[derive(Debug, Serialize, Deserialize)] +struct Config { + name: String, + model: ModelConfig, + retry: RetryConfig, + output: OutputConfig, + prompt: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ModelConfig { + url: String, + name: String, + api_key: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct RetryConfig { + attempts: u32, + delay: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +struct OutputConfig { + descriptions_yaml: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct YamlData { + name: String, + Local: HashMap, + LocalLow: HashMap, + Roaming: HashMap, +} + +const CONFIG: Config = Config { + name: String::from("Xch13"), + model: ModelConfig { + url: String::from("https://xxx/v1/chat/completions"), + name: String::from("xxx"), + api_key: String::from("Bearer xxx"), + }, + retry: RetryConfig { + attempts: 3, + delay: 20, + }, + output: OutputConfig { + descriptions_yaml: String::from("folders_description.yaml"), + }, + prompt: String::from(r#"# 角色: Windows系统专家 + +# 背景信息: 在Windows操作系统中,AppData文件夹通常包含应用程序数据,这些数据可能是设置、配置文件、缓存文件等。 + +# 工作流程/工作任务: +- 识别并分析AppData下的[xxx]文件夹中的[xxx]子文件夹。 +- 研究该子文件夹的具体用途。 +- 提供简洁准确的描述。 + +# 输入提示: 请简述Windows系统中AppData下的[{}]文件夹中的[{}]子文件夹的用途。 + +# 输出示例: +- 软件:Chrome +作用:存储Chrome浏览器的用户数据,如历史记录、书签、密码等。 +功能:用户数据管理 + + +- 软件:Dropbox +作用:存储Dropbox同步的文件和文件夹。 +功能:文件同步与存储 + + +- 软件:Microsoft Office +作用:存储Office应用程序的设置和个性化选项。 +功能:应用程序配置和个性化设置 + + +# 注意事项: +- 描述应简洁明了,不超过50字。 +- 确保描述的准确性,避免误导用户。"#), +}; + +struct YamlManager { + filename: String, + data: YamlData, +} + +impl YamlManager { + fn new(filename: &str, name: &str) -> Result { + let required_data = YamlData { + name: name.to_string(), + Local: HashMap::new(), + LocalLow: HashMap::new(), + Roaming: HashMap::new(), + }; + + let data = if Path::new(filename).exists() { + let content = fs::read_to_string(filename) + .with_context(|| format!("Failed to read file: {}", filename))?; + serde_yaml::from_str(&content).unwrap_or(required_data) + } else { + required_data + }; + + let manager = YamlManager { + filename: filename.to_string(), + data, + }; + manager.save()?; + Ok(manager) + } + + fn add_data(&mut self, section: &str, foldername: &str, description: &str) -> Result<()> { + if section.is_empty() || foldername.is_empty() || description.is_empty() { + println!("Skipping empty data: section={}, folder={}", section, foldername); + return Ok(()); + } + + let section_map = match section { + "Local" => &mut self.data.Local, + "LocalLow" => &mut self.data.LocalLow, + "Roaming" => &mut self.data.Roaming, + _ => { + println!("Invalid section: {}", section); + return Ok(()); + } + }; + + let desc = if description.trim().is_empty() { + "Unknown folder purpose" + } else { + description + }; + + section_map.insert(foldername.to_string(), desc.to_string()); + self.save()?; + Ok(()) + } + + fn save(&self) -> Result<()> { + let yaml_str = serde_yaml::to_string(&self.data)?; + fs::write(&self.filename, yaml_str)?; + Ok(()) + } +} + +struct AppDataAnalyzer { + config: &'static Config, + yaml_manager: YamlManager, +} + +impl AppDataAnalyzer { + fn new() -> Result { + let yaml_manager = YamlManager::new(&CONFIG.output.descriptions_yaml, &CONFIG.name)?; + Ok(AppDataAnalyzer { + config: &CONFIG, + yaml_manager, + }) + } + + async fn analyze_appdata_folder(&self, dir_1: &str, dir_2: &str) -> Result { + let client = reqwest::Client::new(); + let mut headers = HeaderMap::new(); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&self.config.model.api_key)?, + ); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + let data = serde_json::json!({ + "messages": [ + { + "role": "system", + "content": self.config.prompt + }, + { + "role": "user", + "content": format!("请简述Windows系统中AppData下的[{}]文件夹中的[{}]子文件夹的用途。", dir_1, dir_2) + } + ], + "model": self.config.model.name + }); + + let mut attempts = self.config.retry.attempts; + while attempts > 0 { + match client + .post(&self.config.model.url) + .headers(headers.clone()) + .json(&data) + .send() + .await + { + Ok(response) => { + let json = response.json::().await?; + if let Some(content) = json["choices"][0]["message"]["content"].as_str() { + return Ok(content.to_string()); + } + return Ok("Unknown folder purpose".to_string()); + } + Err(e) => { + attempts -= 1; + if attempts > 0 { + println!( + "Analysis error, remaining attempts: {}, error: {}", + attempts, e + ); + thread::sleep(Duration::from_secs(self.config.retry.delay)); + } else { + println!("Analysis finally failed: {}", e); + return Ok("Error occurred during analysis".to_string()); + } + } + } + } + Ok("Error occurred during analysis".to_string()) + } + + fn get_appdata_paths() -> Result> { + let appdata = env::var("APPDATA").context("Failed to get APPDATA environment variable")?; + let base_path = Path::new(&appdata) + .parent() + .context("Failed to get parent directory of APPDATA")? + .to_string_lossy() + .into_owned(); + + let mut paths = HashMap::new(); + paths.insert( + "Local".to_string(), + Path::new(&base_path).join("Local").to_string_lossy().into_owned(), + ); + paths.insert( + "LocalLow".to_string(), + Path::new(&base_path) + .join("LocalLow") + .to_string_lossy() + .into_owned(), + ); + paths.insert("Roaming".to_string(), appdata); + + Ok(paths) + } + + fn list_directories(base_path: &str) -> Vec { + match fs::read_dir(base_path) { + Ok(entries) => entries + .filter_map(|entry| { + entry.ok().and_then(|e| { + if e.path().is_dir() { + e.file_name().into_string().ok() + } else { + None + } + }) + }) + .collect(), + Err(e) => { + println!("Error accessing {}: {}", base_path, e); + vec!["Cannot access or empty directory".to_string()] + } + } + } + + async fn process_directories(&mut self) -> Result<()> { + let appdata_paths = Self::get_appdata_paths()?; + + for (section, path) in appdata_paths { + let folders = Self::list_directories(&path); + for folder in folders { + if folder == "Cannot access or empty directory" { + continue; + } + + if !self.yaml_manager.data.Local.contains_key(&folder) + && !self.yaml_manager.data.LocalLow.contains_key(&folder) + && !self.yaml_manager.data.Roaming.contains_key(&folder) + { + let res = self.analyze_appdata_folder(§ion, &folder).await?; + self.yaml_manager.add_data(§ion, &folder, &res)?; + println!("New saved {} {}:\n{}", section, folder, res); + thread::sleep(Duration::from_secs(1)); + } else { + let existing_description = match section.as_str() { + "Local" => self.yaml_manager.data.Local.get(&folder), + "LocalLow" => self.yaml_manager.data.LocalLow.get(&folder), + "Roaming" => self.yaml_manager.data.Roaming.get(&folder), + _ => None, + }; + if let Some(desc) = existing_description { + println!("Already saved {} {}:\n{}", section, folder, desc); + } + } + } + } + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let mut analyzer = AppDataAnalyzer::new()?; + analyzer.process_directories().await?; + Ok(()) +} \ No newline at end of file From f9679aab9f42c4de7e5240c9fbe989e42c82ea09 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:23:40 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0AI=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增AI配置界面 - 添加API地址、密钥和模型配置输入框 - 添加配置状态显示 - 优化输入提示文本 --- src/ui.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 6a8b663..bf8160e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -16,6 +16,10 @@ pub struct AppDataCleaner { is_scanning: bool, current_folder: Option, folder_data: Vec<(String, u64)>, + show_ai_config_window: bool, // 新增字段,用于ai配置 + ai_url: String, // 新增字段,用于ai配置 + ai_api_key: String, // 新增字段,用于ai配置 + ai_model: String, // 新增字段,用于ai配置 show_about_window: bool, // 确保字段存在 confirm_delete: Option<(String, bool)>, // 保存要确认删除的文件夹状态 selected_appdata_folder: String, // 新增字段 @@ -40,6 +44,10 @@ impl Default for AppDataCleaner { is_scanning: false, current_folder: None, folder_data: vec![], + show_ai_config_window: false, // 默认值 + ai_url: String::new(), + ai_api_key: String::new(), + ai_model: String::new(), show_about_window: false, // 默认值 confirm_delete: None, // 初始化为 None selected_appdata_folder: "Roaming".to_string(), // 默认值为 Roaming @@ -113,10 +121,16 @@ impl eframe::App for AppDataCleaner { // 顶部菜单 egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - if ui.button("关于").clicked() { - self.show_about_window = true; // 打开关于窗口 - ui.close_menu(); - } + ui.horizontal(|ui| { // 使用 horizontal 布局让按钮并排 + if ui.button("关于").clicked() { + self.show_about_window = true; + ui.close_menu(); + } + if ui.button("AI配置").clicked() { + self.show_ai_config_window = true; + ui.close_menu(); + } + }); ui.separator(); ui.checkbox(&mut self.is_logging_enabled, "启用日志"); @@ -282,7 +296,66 @@ impl eframe::App for AppDataCleaner { about::show_about_window(ctx, &mut self.show_about_window); } + // 新增:AI配置窗口 + if self.show_ai_config_window { + egui::Window::new("AI配置") + .resizable(true) + .collapsible(true) + .show(ctx, |ui| { + ui.heading("AI配置生成器"); + + // URL 配置 + ui.horizontal(|ui| { + ui.label("API地址:"); + ui.add(egui::TextEdit::singleline(&mut self.ai_url) + .hint_text("输入API地址,例如:https://api.openai.com/v1")); + }); + + // API Key 配置 + ui.horizontal(|ui| { + ui.label("API密钥:"); + // 使用 password 模式来隐藏 API key + ui.add(egui::TextEdit::singleline(&mut self.ai_api_key) + .password(true) + .hint_text("输入API密钥")); + }); + + // Model 配置 + ui.horizontal(|ui| { + ui.label("模型名称:"); + ui.add(egui::TextEdit::singleline(&mut self.ai_model) + .hint_text("输入模型名称,例如:gpt-3.5-turbo")); + }); + + // 添加保存和测试按钮 + ui.horizontal(|ui| { + if ui.button("保存配置").clicked() { + // TODO: 实现配置保存逻辑 + logger::log_info("AI配置已保存"); + } + + if ui.button("测试连接").clicked() { + // TODO: 实现API连接测试逻辑 + logger::log_info("正在测试AI连接..."); + } + + if ui.button("关闭").clicked() { + self.show_ai_config_window = false; + } + }); + + // 显示配置状态 + ui.separator(); + ui.label("配置状态:"); + if self.ai_url.is_empty() || self.ai_api_key.is_empty() || self.ai_model.is_empty() { + ui.colored_label(egui::Color32::RED, "⚠ 配置不完整"); + } else { + ui.colored_label(egui::Color32::GREEN, "✓ 配置已完成"); + } + }); + } + // 显示移动窗口 self.move_module.show_move_window(ctx); } -} +} \ No newline at end of file From 79170678a35c0df35771a76576ea2c548ed53f95 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:27:25 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad7a9c6..35a7f9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,4 @@ sha2 = "0.10" native-dialog = "0.7.0" serde = { version = "1.0.216", features = ["derive"] } serde_yaml = "0.9.34+deprecated" -serde_json = "1.0" reqwest = { version = "0.11", features = ["json"] } -anyhow = "1.0" \ No newline at end of file From 4c0e2421d9267017e56b22aa7451d45d9dcad367 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:28:24 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai_config.rs | 472 +++++++++++++++++++++++------------------------ src/main.rs | 1 + src/ui.rs | 237 ++++++++++++++++++++---- 3 files changed, 427 insertions(+), 283 deletions(-) diff --git a/src/ai_config.rs b/src/ai_config.rs index 804aee5..d6c7db2 100644 --- a/src/ai_config.rs +++ b/src/ai_config.rs @@ -1,309 +1,297 @@ -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; use serde::{Deserialize, Serialize}; -use serde_yaml; -use std::{collections::HashMap, env, fs, path::Path, thread, time::Duration}; -use tokio; -use anyhow::{Result, Context}; - -#[derive(Debug, Serialize, Deserialize)] -struct Config { - name: String, - model: ModelConfig, - retry: RetryConfig, - output: OutputConfig, - prompt: String, +use std::collections::HashMap; +use std::time::Duration; + +// 为 AIConfig 添加 Clone trait +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AIConfig { + pub name: String, + pub model: ModelConfig, + pub retry: RetryConfig, + pub Local: HashMap, + pub LocalLow: HashMap, + pub Roaming: HashMap, } -#[derive(Debug, Serialize, Deserialize)] -struct ModelConfig { - url: String, - name: String, - api_key: String, +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ModelConfig { + pub url: String, + pub api_key: String, + pub model: String, + pub prompt: String, } -#[derive(Debug, Serialize, Deserialize)] -struct RetryConfig { - attempts: u32, - delay: u64, +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RetryConfig { + pub attempts: u32, + pub delay: u32, } -#[derive(Debug, Serialize, Deserialize)] -struct OutputConfig { - descriptions_yaml: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct YamlData { - name: String, - Local: HashMap, - LocalLow: HashMap, - Roaming: HashMap, -} - -const CONFIG: Config = Config { - name: String::from("Xch13"), - model: ModelConfig { - url: String::from("https://xxx/v1/chat/completions"), - name: String::from("xxx"), - api_key: String::from("Bearer xxx"), - }, - retry: RetryConfig { - attempts: 3, - delay: 20, - }, - output: OutputConfig { - descriptions_yaml: String::from("folders_description.yaml"), - }, - prompt: String::from(r#"# 角色: Windows系统专家 +impl Default for AIConfig { + fn default() -> Self { + Self { + name: String::new(), + model: ModelConfig { + url: "https://api.openai.com/v1".to_string(), + api_key: "your_api_key_here".to_string(), + model: "gpt-3.5-turbo".to_string(), + prompt: r#"# 角色: Windows系统专家 # 背景信息: 在Windows操作系统中,AppData文件夹通常包含应用程序数据,这些数据可能是设置、配置文件、缓存文件等。 -# 工作流程/工作任务: +# 工作流程/工作任务: - 识别并分析AppData下的[xxx]文件夹中的[xxx]子文件夹。 - 研究该子文件夹的具体用途。 - 提供简洁准确的描述。 # 输入提示: 请简述Windows系统中AppData下的[{}]文件夹中的[{}]子文件夹的用途。 -# 输出示例: +# 输出示例: - 软件:Chrome 作用:存储Chrome浏览器的用户数据,如历史记录、书签、密码等。 功能:用户数据管理 - - 软件:Dropbox 作用:存储Dropbox同步的文件和文件夹。 功能:文件同步与存储 - - 软件:Microsoft Office 作用:存储Office应用程序的设置和个性化选项。 功能:应用程序配置和个性化设置 - -# 注意事项: +# 注意事项: - 描述应简洁明了,不超过50字。 -- 确保描述的准确性,避免误导用户。"#), -}; - -struct YamlManager { - filename: String, - data: YamlData, -} - -impl YamlManager { - fn new(filename: &str, name: &str) -> Result { - let required_data = YamlData { - name: name.to_string(), +- 确保描述的准确性,避免误导用户。"# + .to_string(), + }, + retry: RetryConfig { + attempts: 3, + delay: 20, + }, Local: HashMap::new(), LocalLow: HashMap::new(), Roaming: HashMap::new(), - }; + } + } +} - let data = if Path::new(filename).exists() { - let content = fs::read_to_string(filename) - .with_context(|| format!("Failed to read file: {}", filename))?; - serde_yaml::from_str(&content).unwrap_or(required_data) - } else { - required_data - }; +impl AIConfig { + pub fn new() -> Self { + Self::default() + } - let manager = YamlManager { - filename: filename.to_string(), - data, - }; - manager.save()?; - Ok(manager) + // 添加获取配置文件路径的辅助函数 + pub fn get_config_path() -> Result> { + let exe_path = std::env::current_exe()?; + let project_dir = exe_path + .parent() + .unwrap() // debug 目录 + .parent() + .unwrap() // target 目录 + .parent() + .unwrap(); // 项目根目录 + Ok(project_dir.join("folders_description.yaml")) + } + + pub fn load_from_file(path: &str) -> Result> { + let content = std::fs::read_to_string(path)?; + let config: AIConfig = serde_yaml::from_str(&content)?; + Ok(config) } - fn add_data(&mut self, section: &str, foldername: &str, description: &str) -> Result<()> { - if section.is_empty() || foldername.is_empty() || description.is_empty() { - println!("Skipping empty data: section={}, folder={}", section, foldername); - return Ok(()); + pub fn save_to_file(&self, path: &str) -> Result<(), Box> { + // 确保目标目录存在 + if let Some(parent) = std::path::Path::new(path).parent() { + std::fs::create_dir_all(parent)?; } - let section_map = match section { - "Local" => &mut self.data.Local, - "LocalLow" => &mut self.data.LocalLow, - "Roaming" => &mut self.data.Roaming, - _ => { - println!("Invalid section: {}", section); - return Ok(()); - } - }; + // 序列化配置 + let content = serde_yaml::to_string(self)?; - let desc = if description.trim().is_empty() { - "Unknown folder purpose" - } else { - description - }; + // 写入文件 + std::fs::write(path, content)?; + + Ok(()) + } + + pub fn create_default_config( + name: Option, + api_key: Option, + model: Option, + ) -> Result<(), Box> { + let mut config = Self::default(); + + // 使用提供的参数更新配置 + if let Some(name) = name { + config.name = name; + } + if let Some(api_key) = api_key { + config.model.api_key = api_key; + } + if let Some(model) = model { + config.model.model = model; + } + + // 直接在当前目录创建配置文件 + config.save_to_file(".\\folders_description.yaml")?; - section_map.insert(foldername.to_string(), desc.to_string()); - self.save()?; Ok(()) } - fn save(&self) -> Result<()> { - let yaml_str = serde_yaml::to_string(&self.data)?; - fs::write(&self.filename, yaml_str)?; + pub fn validate(&self) -> Result<(), String> { + if self.name.trim().is_empty() { + return Err("配置名称不能为空".to_string()); + } + if self.model.url.trim().is_empty() { + return Err("API地址不能为空".to_string()); + } + if self.model.api_key.trim().is_empty() { + return Err("API密钥不能为空".to_string()); + } + if self.model.model.trim().is_empty() { + return Err("模型名称不能为空".to_string()); + } Ok(()) } } -struct AppDataAnalyzer { - config: &'static Config, - yaml_manager: YamlManager, +// API 请求相关结构体 +#[derive(Debug, Serialize, Deserialize)] +pub struct Message { + pub role: String, + pub content: String, } -impl AppDataAnalyzer { - fn new() -> Result { - let yaml_manager = YamlManager::new(&CONFIG.output.descriptions_yaml, &CONFIG.name)?; - Ok(AppDataAnalyzer { - config: &CONFIG, - yaml_manager, - }) +#[derive(Debug, Serialize)] +pub struct ChatRequest { + pub messages: Vec, + pub model: String, +} + +#[derive(Debug, Deserialize)] +pub struct ChatResponse { + pub choices: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Choice { + pub message: Message, +} + +// AI 客户端结构体 +#[derive(Debug)] +pub struct AIClient { + config: AIConfig, + client: reqwest::Client, +} + +// 实现 AIClient +impl AIClient { + pub fn new(config: AIConfig) -> Self { + Self { + config, + client: reqwest::Client::new(), + } } - async fn analyze_appdata_folder(&self, dir_1: &str, dir_2: &str) -> Result { - let client = reqwest::Client::new(); - let mut headers = HeaderMap::new(); - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&self.config.model.api_key)?, - ); - headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - - let data = serde_json::json!({ - "messages": [ - { - "role": "system", - "content": self.config.prompt - }, - { - "role": "user", - "content": format!("请简述Windows系统中AppData下的[{}]文件夹中的[{}]子文件夹的用途。", dir_1, dir_2) - } - ], - "model": self.config.model.name - }); - - let mut attempts = self.config.retry.attempts; - while attempts > 0 { - match client - .post(&self.config.model.url) - .headers(headers.clone()) - .json(&data) - .send() - .await - { - Ok(response) => { - let json = response.json::().await?; - if let Some(content) = json["choices"][0]["message"]["content"].as_str() { - return Ok(content.to_string()); - } - return Ok("Unknown folder purpose".to_string()); - } + // 发送请求到 AI API 并处理重试 + pub async fn get_folder_description( + &self, + dir_1: &str, + dir_2: &str, + ) -> Result> { + let mut attempts = 0; + let max_attempts = self.config.retry.attempts; + let delay = Duration::from_secs(self.config.retry.delay as u64); + + loop { + attempts += 1; + match self.try_get_description(dir_1, dir_2).await { + Ok(description) => return Ok(description), Err(e) => { - attempts -= 1; - if attempts > 0 { - println!( - "Analysis error, remaining attempts: {}, error: {}", - attempts, e - ); - thread::sleep(Duration::from_secs(self.config.retry.delay)); - } else { - println!("Analysis finally failed: {}", e); - return Ok("Error occurred during analysis".to_string()); + if attempts >= max_attempts { + return Err(format!("达到最大重试次数 {}: {}", max_attempts, e).into()); } + crate::logger::log_error(&format!( + "API请求失败 (尝试 {}/{}): {},将在 {}s 后重试", + attempts, max_attempts, e, delay.as_secs() + )); + tokio::time::sleep(delay).await; + continue; } } } - Ok("Error occurred during analysis".to_string()) } - fn get_appdata_paths() -> Result> { - let appdata = env::var("APPDATA").context("Failed to get APPDATA environment variable")?; - let base_path = Path::new(&appdata) - .parent() - .context("Failed to get parent directory of APPDATA")? - .to_string_lossy() - .into_owned(); - - let mut paths = HashMap::new(); - paths.insert( - "Local".to_string(), - Path::new(&base_path).join("Local").to_string_lossy().into_owned(), - ); - paths.insert( - "LocalLow".to_string(), - Path::new(&base_path) - .join("LocalLow") - .to_string_lossy() - .into_owned(), - ); - paths.insert("Roaming".to_string(), appdata); - - Ok(paths) - } + // 单次请求实现 + async fn try_get_description( + &self, + dir_1: &str, + dir_2: &str, + ) -> Result> { + let request = ChatRequest { + messages: vec![ + Message { + role: "system".to_string(), + content: self.config.model.prompt.clone(), + }, + Message { + role: "user".to_string(), + content: format!( + "请简述Windows系统中AppData下的[{}]文件夹中的[{}]子文件夹的用途。", + dir_1, dir_2 + ), + }, + ], + model: self.config.model.model.clone(), + }; - fn list_directories(base_path: &str) -> Vec { - match fs::read_dir(base_path) { - Ok(entries) => entries - .filter_map(|entry| { - entry.ok().and_then(|e| { - if e.path().is_dir() { - e.file_name().into_string().ok() - } else { - None - } - }) - }) - .collect(), - Err(e) => { - println!("Error accessing {}: {}", base_path, e); - vec!["Cannot access or empty directory".to_string()] - } + let response = self.client + .post(&self.config.model.url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.model.api_key)) + .json(&request) + .send() + .await?; + + if !response.status().is_success() { + return Err(format!( + "API请求失败: {} - {}", + response.status(), + response.text().await? + ).into()); + } + + let chat_response: ChatResponse = response.json().await?; + if let Some(choice) = chat_response.choices.first() { + Ok(choice.message.content.clone()) + } else { + Err("API返回空响应".into()) } } - async fn process_directories(&mut self) -> Result<()> { - let appdata_paths = Self::get_appdata_paths()?; + // 测试连接 + pub async fn test_connection(&self) -> Result<(), Box> { + let request = ChatRequest { + messages: vec![Message { + role: "user".to_string(), + content: "测试连接".to_string(), + }], + model: self.config.model.model.clone(), + }; - for (section, path) in appdata_paths { - let folders = Self::list_directories(&path); - for folder in folders { - if folder == "Cannot access or empty directory" { - continue; - } + let response = self.client + .post(&self.config.model.url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.config.model.api_key)) + .json(&request) + .send() + .await?; - if !self.yaml_manager.data.Local.contains_key(&folder) - && !self.yaml_manager.data.LocalLow.contains_key(&folder) - && !self.yaml_manager.data.Roaming.contains_key(&folder) - { - let res = self.analyze_appdata_folder(§ion, &folder).await?; - self.yaml_manager.add_data(§ion, &folder, &res)?; - println!("New saved {} {}:\n{}", section, folder, res); - thread::sleep(Duration::from_secs(1)); - } else { - let existing_description = match section.as_str() { - "Local" => self.yaml_manager.data.Local.get(&folder), - "LocalLow" => self.yaml_manager.data.LocalLow.get(&folder), - "Roaming" => self.yaml_manager.data.Roaming.get(&folder), - _ => None, - }; - if let Some(desc) = existing_description { - println!("Already saved {} {}:\n{}", section, folder, desc); - } - } - } + if response.status().is_success() { + Ok(()) + } else { + Err(format!("API连接测试失败: {}", response.status()).into()) } - Ok(()) } -} - -#[tokio::main] -async fn main() -> Result<()> { - let mut analyzer = AppDataAnalyzer::new()?; - analyzer.process_directories().await?; - Ok(()) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 638bd26..f5906d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod scanner; // 引入扫盘模块 mod ui; // 引入 ui 模块 mod utils; // 文件夹大小计算模块 mod yaml_loader; // 文件描述 +pub mod ai_config; // 只需要保留这一行,使用 pub 使其可以被其他模块访问 use ui::AppDataCleaner; diff --git a/src/ui.rs b/src/ui.rs index bf8160e..a4d1909 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,5 @@ use crate::about; +use crate::ai_config::{AIConfig, AIClient}; // 添加 AIClient use crate::confirmation; use crate::delete; use crate::ignore; @@ -35,34 +36,61 @@ pub struct AppDataCleaner { sort_criterion: Option, // 新增字段,排序标准 "name" 或 "size" sort_order: Option, // 新增字段,排序顺序 "asc" 或 "desc" total_size: u64, // 新增字段,总大小 + ai_config: AIConfig, + ai_retry_attempts: u32, + ai_retry_delay: u32, + show_prompt_editor: bool, + ai_client: Option, // 在 AppDataCleaner 结构体中添加 ai_client 字段 } impl Default for AppDataCleaner { fn default() -> Self { let (tx, rx) = std::sync::mpsc::channel(); + + // 尝试加载配置文件 + let ai_config = match AIConfig::load_from_file("folders_description.yaml") { + Ok(config) => { + logger::log_info("已成功加载AI配置文件"); + config + } + Err(_) => { + logger::log_info("未找到配置文件,使用默认配置"); + AIConfig::default() + } + }; + + // 先保存重试配置的值 + let attempts = ai_config.retry.attempts; + let delay = ai_config.retry.delay; + Self { is_scanning: false, current_folder: None, folder_data: vec![], - show_ai_config_window: false, // 默认值 + show_ai_config_window: false, ai_url: String::new(), ai_api_key: String::new(), ai_model: String::new(), - show_about_window: false, // 默认值 - confirm_delete: None, // 初始化为 None - selected_appdata_folder: "Roaming".to_string(), // 默认值为 Roaming + show_about_window: false, + confirm_delete: None, + selected_appdata_folder: "Roaming".to_string(), tx: Some(tx), rx: Some(rx), - is_logging_enabled: false, // 默认禁用日志 - previous_logging_state: false, // 初始时假定日志系统未启用 + is_logging_enabled: false, + previous_logging_state: false, ignored_folders: ignore::load_ignored_folders(), move_module: Default::default(), folder_descriptions: None, - yaml_error_logged: false, // 初始时假定未记录过错误 - status: Some("未扫描".to_string()), // 初始化为 "未扫描" - sort_criterion: None, // 初始化为 None - sort_order: None, // 初始化为 None - total_size: 0, // 初始化为 0 + yaml_error_logged: false, + status: Some("未扫描".to_string()), + sort_criterion: None, + sort_order: None, + total_size: 0, + ai_config, // 移动 ai_config + ai_retry_attempts: attempts, // 使用保存的值 + ai_retry_delay: delay, // 使用保存的值 + show_prompt_editor: false, + ai_client: None, // 初始化 ai_client 字段 } } } @@ -301,57 +329,184 @@ impl eframe::App for AppDataCleaner { egui::Window::new("AI配置") .resizable(true) .collapsible(true) + .min_width(400.0) // 添加最小宽度 + .min_height(500.0) // 添加最小高度 .show(ctx, |ui| { ui.heading("AI配置生成器"); - // URL 配置 - ui.horizontal(|ui| { - ui.label("API地址:"); - ui.add(egui::TextEdit::singleline(&mut self.ai_url) - .hint_text("输入API地址,例如:https://api.openai.com/v1")); + // 基本配置 + ui.group(|ui| { // 将基本配置也放入组中 + ui.heading("基本设置"); + ui.horizontal(|ui| { + ui.label("配置名称:"); + ui.add(egui::TextEdit::singleline(&mut self.ai_config.name) + .hint_text("输入配置名称") // 添加提示文本 + .desired_width(200.0)); // 设置输入框宽度 + }); }); + + // API配置组 + ui.group(|ui| { + ui.heading("API设置"); + ui.horizontal(|ui| { + ui.label("API地址:"); + ui.add(egui::TextEdit::singleline(&mut self.ai_config.model.url) + .hint_text("输入 API 地址,如 https://api.openai.com/v1") + .desired_width(250.0)); + }); - // API Key 配置 - ui.horizontal(|ui| { - ui.label("API密钥:"); - // 使用 password 模式来隐藏 API key - ui.add(egui::TextEdit::singleline(&mut self.ai_api_key) - .password(true) - .hint_text("输入API密钥")); + ui.horizontal(|ui| { + ui.label("API密钥:"); + ui.add(egui::TextEdit::singleline(&mut self.ai_config.model.api_key) + .password(true) + .hint_text("输入你的API密钥") + .desired_width(250.0)); + }); + + ui.horizontal(|ui| { + ui.label("模型名称:"); + ui.add(egui::TextEdit::singleline(&mut self.ai_config.model.model) + .hint_text("输入模型名称,如 gpt-3.5-turbo") + .desired_width(250.0)); + }); }); - // Model 配置 - ui.horizontal(|ui| { - ui.label("模型名称:"); - ui.add(egui::TextEdit::singleline(&mut self.ai_model) - .hint_text("输入模型名称,例如:gpt-3.5-turbo")); + // 重试配置组 + ui.group(|ui| { + ui.heading("重试设置"); + ui.horizontal(|ui| { + ui.label("重试次数:"); + ui.add(egui::DragValue::new(&mut self.ai_config.retry.attempts) + .range(1..=10) // 使用 range 替代 clamp_range + .speed(1) + .prefix("次数: ")); + }); + + ui.horizontal(|ui| { + ui.label("重试延迟:"); + ui.add(egui::DragValue::new(&mut self.ai_config.retry.delay) + .range(1..=60) // 使用 range 替代 clamp_range + .speed(1) + .suffix(" 秒")); + }); + }); + + // Prompt编辑器按钮 + ui.group(|ui| { + ui.heading("Prompt设置"); + if ui.button("编辑Prompt模板").clicked() { + self.show_prompt_editor = true; + } + // 显示当前prompt的预览 + ui.label("当前模板预览:"); + ui.add(egui::TextEdit::multiline(&mut self.ai_config.model.prompt.clone()) + .desired_width(f32::INFINITY) + .desired_rows(3) + .interactive(false)); // 使用 interactive(false) 替代 read_only }); - // 添加保存和测试按钮 + ui.add_space(10.0); // 添加一些间距 + + // 按钮组 ui.horizontal(|ui| { if ui.button("保存配置").clicked() { - // TODO: 实现配置保存逻辑 - logger::log_info("AI配置已保存"); + match self.ai_config.validate() { + Ok(_) => { + match AIConfig::get_config_path() { + Ok(config_path) => { + match self.ai_config.save_to_file(config_path.to_str().unwrap()) { + Ok(_) => { + logger::log_info(&format!( + "AI配置已保存到: {}", + config_path.display() + )); + self.status = Some("配置已保存".to_string()); + } + Err(err) => { + logger::log_error(&format!( + "保存配置失败: {}, 路径: {}", + err, + config_path.display() + )); + self.status = Some("保存配置失败".to_string()); + } + } + } + Err(err) => { + logger::log_error(&format!("获取配置路径失败: {}", err)); + self.status = Some("保存配置失败".to_string()); + } + } + } + Err(err) => { + logger::log_error(&format!("配置验证失败: {}", err)); + self.status = Some(format!("错误: {}", err)); + } + } } if ui.button("测试连接").clicked() { - // TODO: 实现API连接测试逻辑 - logger::log_info("正在测试AI连接..."); + let client = AIClient::new(self.ai_config.clone()); + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { + match client.test_connection().await { + Ok(_) => { + logger::log_info("AI连接测试成功"); + self.status = Some("AI连接测试成功".to_string()); + } + Err(err) => { + logger::log_error(&format!("AI连接测试失败: {}", err)); + self.status = Some(format!("AI连接测试失败: {}", err)); + } + } + }); + } + + if ui.button("重置默认值").clicked() { + self.ai_config = AIConfig::default(); } if ui.button("关闭").clicked() { self.show_ai_config_window = false; } }); + }); + } - // 显示配置状态 - ui.separator(); - ui.label("配置状态:"); - if self.ai_url.is_empty() || self.ai_api_key.is_empty() || self.ai_model.is_empty() { - ui.colored_label(egui::Color32::RED, "⚠ 配置不完整"); - } else { - ui.colored_label(egui::Color32::GREEN, "✓ 配置已完成"); - } + // Prompt编辑器窗口也添加边界 + if self.show_prompt_editor { + egui::Window::new("Prompt模板编辑器") + .resizable(true) + .min_width(600.0) + .min_height(400.0) + .show(ctx, |ui| { + ui.label("编辑Prompt模板:"); + ui.add_space(5.0); + + let mut prompt = self.ai_config.model.prompt.clone(); + ui.add( + egui::TextEdit::multiline(&mut prompt) + .desired_width(f32::INFINITY) + .desired_rows(20) + .font(egui::TextStyle::Monospace) // 使用等宽字体 + ); + self.ai_config.model.prompt = prompt; + + ui.add_space(10.0); + ui.horizontal(|ui| { + if ui.button("保存").clicked() { + self.show_prompt_editor = false; + } + if ui.button("重置默认值").clicked() { + self.ai_config.model.prompt = AIConfig::default().model.prompt; + } + if ui.button("取消").clicked() { + self.show_prompt_editor = false; + self.ai_config.model.prompt = AIConfig::default().model.prompt; + } + }); }); } From abd22ddd9a25559b2088999c5688ce4508000d60 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:10:16 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86ai=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/ui.rs b/src/ui.rs index a4d1909..78101ab 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -158,6 +158,48 @@ impl eframe::App for AppDataCleaner { self.show_ai_config_window = true; ui.close_menu(); } + if ui.button("一键生成所有描述").clicked() { + // 确保已经初始化了 AI 客户端 + if self.ai_client.is_none() { + self.ai_client = Some(AIClient::new(self.ai_config.clone())); + } + + // 创建一个新的运行时来处理异步操作 + if let Some(client) = &self.ai_client { + let folder_data = self.folder_data.clone(); + let selected_folder = self.selected_appdata_folder.clone(); + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { + for (folder, _) in folder_data { + match client.get_folder_description(&selected_folder, &folder).await { + Ok(description) => { + // 根据文件夹类型更新相应的 HashMap + match selected_folder.as_str() { + "Local" => self.ai_config.Local.insert(folder.clone(), description), + "LocalLow" => self.ai_config.LocalLow.insert(folder.clone(), description), + "Roaming" => self.ai_config.Roaming.insert(folder.clone(), description), + _ => None, + }; + } + Err(e) => { + logger::log_error(&format!("生成描述失败 {}: {}", folder, e)); + } + } + } + + // 保存更新后的配置 + if let Ok(config_path) = AIConfig::get_config_path() { + if let Err(e) = self.ai_config.save_to_file(config_path.to_str().unwrap()) { + logger::log_error(&format!("保存配置失败: {}", e)); + } + } + }); + + self.status = Some("所有描述生成完成".to_string()); + } + } }); ui.separator(); @@ -313,6 +355,46 @@ impl eframe::App for AppDataCleaner { } } } + if ui.button("生成描述").clicked() { + // 确保已经初始化了 AI 客户端 + if self.ai_client.is_none() { + self.ai_client = Some(AIClient::new(self.ai_config.clone())); + } + + if let Some(client) = &self.ai_client { + let folder_name = folder.clone(); + let selected_folder = self.selected_appdata_folder.clone(); + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { + match client.get_folder_description(&selected_folder, &folder_name).await { + Ok(description) => { + // 根据文件夹类型更新相应的 HashMap + match selected_folder.as_str() { + "Local" => self.ai_config.Local.insert(folder_name.clone(), description), + "LocalLow" => self.ai_config.LocalLow.insert(folder_name.clone(), description), + "Roaming" => self.ai_config.Roaming.insert(folder_name.clone(), description), + _ => None, + }; + + // 保存更新后的配置 + if let Ok(config_path) = AIConfig::get_config_path() { + if let Err(e) = self.ai_config.save_to_file(config_path.to_str().unwrap()) { + logger::log_error(&format!("保存配置失败: {}", e)); + } + } + + self.status = Some(format!("已生成 {} 的描述", folder_name)); + } + Err(e) => { + logger::log_error(&format!("生成描述失败 {}: {}", folder_name, e)); + self.status = Some(format!("生成描述失败: {}", e)); + } + } + }); + } + } ui.end_row(); } }); From 32469e52c8ed77c4fcd1a3b8927673422c5cd964 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:37:28 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ai=E8=A7=A3=E9=87=8A?= =?UTF-8?q?=E7=9A=84GUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui.rs | 225 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 152 insertions(+), 73 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 78101ab..6ad6037 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,9 @@ use crate::about; -use crate::ai_config::{AIConfig, AIClient}; // 添加 AIClient +use crate::ai_config::{AIConfig, AIClient}; use crate::confirmation; -use crate::delete; use crate::ignore; -use crate::logger; // 导入 logger 模块 -use crate::move_module; // 导入移动模块 +use crate::logger; +use crate::move_module; use crate::open; use crate::scanner; use crate::utils; @@ -41,11 +40,14 @@ pub struct AppDataCleaner { ai_retry_delay: u32, show_prompt_editor: bool, ai_client: Option, // 在 AppDataCleaner 结构体中添加 ai_client 字段 + ai_tx: Option>, // (folder_type, folder_name, description) + ai_rx: Option>, } impl Default for AppDataCleaner { fn default() -> Self { let (tx, rx) = std::sync::mpsc::channel(); + let (ai_tx, ai_rx) = std::sync::mpsc::channel(); // 尝试加载配置文件 let ai_config = match AIConfig::load_from_file("folders_description.yaml") { @@ -91,6 +93,8 @@ impl Default for AppDataCleaner { ai_retry_delay: delay, // 使用保存的值 show_prompt_editor: false, ai_client: None, // 初始化 ai_client 字段 + ai_tx: Some(ai_tx), + ai_rx: Some(ai_rx), } } } @@ -159,46 +163,75 @@ impl eframe::App for AppDataCleaner { ui.close_menu(); } if ui.button("一键生成所有描述").clicked() { - // 确保已经初始化了 AI 客户端 - if self.ai_client.is_none() { - self.ai_client = Some(AIClient::new(self.ai_config.clone())); - } - - // 创建一个新的运行时来处理异步操作 - if let Some(client) = &self.ai_client { - let folder_data = self.folder_data.clone(); - let selected_folder = self.selected_appdata_folder.clone(); - - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async { - for (folder, _) in folder_data { - match client.get_folder_description(&selected_folder, &folder).await { - Ok(description) => { - // 根据文件夹类型更新相应的 HashMap - match selected_folder.as_str() { - "Local" => self.ai_config.Local.insert(folder.clone(), description), - "LocalLow" => self.ai_config.LocalLow.insert(folder.clone(), description), - "Roaming" => self.ai_config.Roaming.insert(folder.clone(), description), - _ => None, - }; - } - Err(e) => { - logger::log_error(&format!("生成描述失败 {}: {}", folder, e)); + let folder_data = self.folder_data.clone(); + let selected_folder = self.selected_appdata_folder.clone(); + let mut ai_config = self.ai_config.clone(); + let ai_tx = self.ai_tx.clone(); + + // 更新状态提示正在处理 + self.status = Some("正在生成描述...".to_string()); + + // 创建一个新的客户端实例 + let client = AIClient::new(ai_config.clone()); + + // 创建一个后台任务 + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + for (folder, _) in folder_data { + logger::log_info(&format!("开始为 {} 生成描述", folder)); + + match client.get_folder_description(&selected_folder, &folder).await { + Ok(description) => { + logger::log_info(&format!( + "成功生成描述 - {}/{}: {}", + selected_folder, + folder, + description + )); + + // 更新配置 + match selected_folder.as_str() { + "Local" => ai_config.Local.insert(folder.clone(), description.clone()), + "LocalLow" => ai_config.LocalLow.insert(folder.clone(), description.clone()), + "Roaming" => ai_config.Roaming.insert(folder.clone(), description.clone()), + _ => None, + }; + + // 立即保存配置到文件 + if let Ok(config_path) = AIConfig::get_config_path() { + match ai_config.save_to_file(config_path.to_str().unwrap()) { + Ok(_) => { + logger::log_info("配置文件保存成功"); + // 发送更新消息到 UI + if let Some(tx) = &ai_tx { + let _ = tx.send((selected_folder.clone(), folder.clone(), description)); + } + } + Err(e) => logger::log_error(&format!("保存配置失败: {}", e)), + } } } - } - - // 保存更新后的配置 - if let Ok(config_path) = AIConfig::get_config_path() { - if let Err(e) = self.ai_config.save_to_file(config_path.to_str().unwrap()) { - logger::log_error(&format!("保存配置失败: {}", e)); + Err(e) => { + logger::log_error(&format!( + "生成描述失败 {}/{}: {}", + selected_folder, + folder, + e + )); } } - }); - - self.status = Some("所有描述生成完成".to_string()); - } + } + + // 保存更新后的配置 + if let Ok(config_path) = AIConfig::get_config_path() { + match ai_config.save_to_file(config_path.to_str().unwrap()) { + Ok(_) => logger::log_info("配置文件保存成功"), + Err(e) => logger::log_error(&format!("保存配置失败: {}", e)), + } + } + }); + }); } }); @@ -356,44 +389,65 @@ impl eframe::App for AppDataCleaner { } } if ui.button("生成描述").clicked() { - // 确保已经初始化了 AI 客户端 - if self.ai_client.is_none() { - self.ai_client = Some(AIClient::new(self.ai_config.clone())); - } + let folder_name = folder.clone(); + let selected_folder = self.selected_appdata_folder.clone(); + let mut ai_config = self.ai_config.clone(); + let ai_tx = self.ai_tx.clone(); + + // 更新状态提示正在处理 + self.status = Some(format!("正在为 {} 生成描述...", folder_name)); + + // 创建一个新的客户端实例 + let client = AIClient::new(ai_config.clone()); + + // 创建一个后台任务 + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + logger::log_info(&format!("开始为 {} 生成描述", folder_name)); + + match client.get_folder_description(&selected_folder, &folder_name).await { + Ok(description) => { + logger::log_info(&format!( + "成功生成描述 - {}/{}: {}", + selected_folder, + folder_name, + description + )); + + // 更新配置 + match selected_folder.as_str() { + "Local" => { ai_config.Local.insert(folder_name.clone(), description.clone()); } + "LocalLow" => { ai_config.LocalLow.insert(folder_name.clone(), description.clone()); } + "Roaming" => { ai_config.Roaming.insert(folder_name.clone(), description.clone()); } + _ => {} + }; - if let Some(client) = &self.ai_client { - let folder_name = folder.clone(); - let selected_folder = self.selected_appdata_folder.clone(); - - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async { - match client.get_folder_description(&selected_folder, &folder_name).await { - Ok(description) => { - // 根据文件夹类型更新相应的 HashMap - match selected_folder.as_str() { - "Local" => self.ai_config.Local.insert(folder_name.clone(), description), - "LocalLow" => self.ai_config.LocalLow.insert(folder_name.clone(), description), - "Roaming" => self.ai_config.Roaming.insert(folder_name.clone(), description), - _ => None, - }; - - // 保存更新后的配置 - if let Ok(config_path) = AIConfig::get_config_path() { - if let Err(e) = self.ai_config.save_to_file(config_path.to_str().unwrap()) { - logger::log_error(&format!("保存配置失败: {}", e)); + // 立即保存配置到文件 + if let Ok(config_path) = AIConfig::get_config_path() { + match ai_config.save_to_file(config_path.to_str().unwrap()) { + Ok(_) => { + logger::log_info("配置文件保存成功"); + // 发送更新消息到 UI + if let Some(tx) = &ai_tx { + let _ = tx.send((selected_folder.clone(), folder_name.clone(), description)); + } } + Err(e) => logger::log_error(&format!("保存配置失败: {}", e)), } - - self.status = Some(format!("已生成 {} 的描述", folder_name)); - } - Err(e) => { - logger::log_error(&format!("生成描述失败 {}: {}", folder_name, e)); - self.status = Some(format!("生成描述失败: {}", e)); } } - }); - } + Err(e) => { + logger::log_error(&format!( + "生成描述失败 {}/{}: {}", + selected_folder, + folder_name, + e + )); + } + } + }); + }); } ui.end_row(); } @@ -592,6 +646,31 @@ impl eframe::App for AppDataCleaner { }); } + // 在主循环中处理接收到的更新 + if let Some(rx) = &self.ai_rx { + while let Ok((folder_type, folder_name, description)) = rx.try_recv() { + // 更新本地配置 + match folder_type.as_str() { + "Local" => { self.ai_config.Local.insert(folder_name.clone(), description.clone()); } + "LocalLow" => { self.ai_config.LocalLow.insert(folder_name.clone(), description.clone()); } + "Roaming" => { self.ai_config.Roaming.insert(folder_name.clone(), description.clone()); } + _ => {} + }; + + // 重新加载描述文件 + if let Ok(config) = AIConfig::load_from_file("folders_description.yaml") { + self.ai_config = config; + self.folder_descriptions = load_folder_descriptions("folders_description.yaml", &mut self.yaml_error_logged); + + // 更新状态 + self.status = Some(format!("已更新 {} 的描述", folder_name)); + + // 强制重绘 + ctx.request_repaint(); + } + } + } + // 显示移动窗口 self.move_module.show_move_window(ctx); } From 0a205ff9b2f3b81608be72b085138ff5a8a2cd5d Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:55:51 +0800 Subject: [PATCH 08/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dfolders=5Fdescription.y?= =?UTF-8?q?aml=E5=88=9B=E5=BB=BA=E4=BD=8D=E7=BD=AE=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai_config.rs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/ai_config.rs b/src/ai_config.rs index d6c7db2..828d4fa 100644 --- a/src/ai_config.rs +++ b/src/ai_config.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; +use crate::logger; // 为 AIConfig 添加 Clone trait #[derive(Debug, Serialize, Deserialize, Clone)] @@ -80,17 +81,9 @@ impl AIConfig { Self::default() } - // 添加获取配置文件路径的辅助函数 + // 简化路径处理,直接使用固定路径 pub fn get_config_path() -> Result> { - let exe_path = std::env::current_exe()?; - let project_dir = exe_path - .parent() - .unwrap() // debug 目录 - .parent() - .unwrap() // target 目录 - .parent() - .unwrap(); // 项目根目录 - Ok(project_dir.join("folders_description.yaml")) + Ok(std::path::PathBuf::from("folders_description.yaml")) } pub fn load_from_file(path: &str) -> Result> { @@ -100,17 +93,14 @@ impl AIConfig { } pub fn save_to_file(&self, path: &str) -> Result<(), Box> { - // 确保目标目录存在 - if let Some(parent) = std::path::Path::new(path).parent() { - std::fs::create_dir_all(parent)?; - } - // 序列化配置 let content = serde_yaml::to_string(self)?; // 写入文件 std::fs::write(path, content)?; - + + logger::log_info(&format!("配置文件已保存到: {}", path)); + Ok(()) } @@ -132,8 +122,9 @@ impl AIConfig { config.model.model = model; } - // 直接在当前目录创建配置文件 - config.save_to_file(".\\folders_description.yaml")?; + // 获取正确的配置文件路径 + let config_path = Self::get_config_path()?; + config.save_to_file(config_path.to_str().unwrap())?; Ok(()) } From 29577a2528e522a3cc18958acaa2de8be54fdfdb Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:19:58 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E6=84=8F=E5=A4=96?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=9A=84delete=E6=A8=A1=E5=9D=97=E8=B0=83?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui.rs b/src/ui.rs index 6ad6037..80ce1f0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,7 @@ use crate::about; use crate::ai_config::{AIConfig, AIClient}; use crate::confirmation; +use crate::delete; use crate::ignore; use crate::logger; use crate::move_module; From 7a0f1aaa644170f27b7ab4e596bfa9d359ff12ce Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:03:15 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai_config.rs | 180 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 151 insertions(+), 29 deletions(-) diff --git a/src/ai_config.rs b/src/ai_config.rs index 828d4fa..4872a13 100644 --- a/src/ai_config.rs +++ b/src/ai_config.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::Duration; +use std::sync::mpsc::Sender; // 添加 Sender 导入 +use std::error::Error; // 添加标准错误特征 use crate::logger; // 为 AIConfig 添加 Clone trait @@ -33,36 +35,44 @@ impl Default for AIConfig { Self { name: String::new(), model: ModelConfig { - url: "https://api.openai.com/v1".to_string(), + url: "https://open.bigmodel.cn/api/paas/v4/chat/completions".to_string(), api_key: "your_api_key_here".to_string(), - model: "gpt-3.5-turbo".to_string(), - prompt: r#"# 角色: Windows系统专家 - -# 背景信息: 在Windows操作系统中,AppData文件夹通常包含应用程序数据,这些数据可能是设置、配置文件、缓存文件等。 - -# 工作流程/工作任务: -- 识别并分析AppData下的[xxx]文件夹中的[xxx]子文件夹。 -- 研究该子文件夹的具体用途。 -- 提供简洁准确的描述。 - -# 输入提示: 请简述Windows系统中AppData下的[{}]文件夹中的[{}]子文件夹的用途。 - -# 输出示例: -- 软件:Chrome -作用:存储Chrome浏览器的用户数据,如历史记录、书签、密码等。 -功能:用户数据管理 - -- 软件:Dropbox -作用:存储Dropbox同步的文件和文件夹。 -功能:文件同步与存储 - -- 软件:Microsoft Office -作用:存储Office应用程序的设置和个性化选项。 -功能:应用程序配置和个性化设置 - -# 注意事项: -- 描述应简洁明了,不超过50字。 -- 确保描述的准确性,避免误导用户。"# + model: "glm-4-flash".to_string(), + prompt: r#" # 角色:Windows AppData分析专家 + +您是一个专业的Windows AppData文件夹分析专家。您需要分析用户提供的AppData文件夹信息并按照固定格式回答。 + +## 输入格式验证规则 +当用户输入包含以下要素时视为有效: +1. 包含"AppData"关键词 +2. 包含主目录[Local|LocalLow|Roaming]之一 +3. 包含具体的应用程序文件夹名称 + +## 输出格式 +``` +- 软件名称:<应用程序名称> +- 数据类别:[配置|缓存|用户数据|日志] +- 应用用途:<简要描述(限50字)> +- 管理建议:[是|否]可安全删除 +``` + +## 示例对话 +用户输入:请分析Windows系统中AppData下Local文件夹中的Microsoft文件夹 + +系统输出: +- 软件名称:Microsoft Office +- 数据类别:配置 +- 应用用途:存储Office应用程序的本地设置和临时文件 +- 管理建议:是可安全删除 + +## 处理指令 +1. 对任何符合输入格式的查询,直接使用输出格式回答 +2. 保持输出格式的严格一致性 +3. 不添加任何额外解释或评论 +4. 确保应用用途描述在50字以内 + +## 注意 +仅当输入完全不符合格式要求时,才返回:"请按照正确的输入格式提供查询信息""# .to_string(), }, retry: RetryConfig { @@ -285,4 +295,116 @@ impl AIClient { Err(format!("API连接测试失败: {}", response.status()).into()) } } +} + +// 添加新的AI处理功能结构体 +#[derive(Debug)] +pub struct AIHandler { + config: AIConfig, + client: AIClient, + tx: Option>, +} + +impl AIHandler { + pub fn new(config: AIConfig, tx: Option>) -> Self { + Self { + client: AIClient::new(config.clone()), + config, + tx, + } + } + + // 生成单个文件夹的描述 + pub async fn generate_single_description( + &mut self, + folder_name: String, + selected_folder: String, + ) -> Result<(), Box> { + logger::log_info(&format!("开始为 {} 生成描述", folder_name)); + + match self.client.get_folder_description(&selected_folder, &folder_name).await { + Ok(description) => { + logger::log_info(&format!( + "成功生成描述 - {}/{}: {}", + selected_folder, + folder_name, + description + )); + + // 更新配置 + match selected_folder.as_str() { + "Local" => { self.config.Local.insert(folder_name.clone(), description.clone()); } + "LocalLow" => { self.config.LocalLow.insert(folder_name.clone(), description.clone()); } + "Roaming" => { self.config.Roaming.insert(folder_name.clone(), description.clone()); } + _ => {} + }; + + // 保存配置并通知 + if let Err(e) = self.save_config_and_notify(&selected_folder, &folder_name, &description) { + logger::log_error(&format!("保存配置失败: {}", e)); + } + Ok(()) + } + Err(e) => { + logger::log_error(&format!( + "生成描述失败 {}/{}: {}", + selected_folder, + folder_name, + e + )); + Err(e) + } + } + } + + // 批量生成所有文件夹的描述 + pub async fn generate_all_descriptions( + &mut self, + folder_data: Vec<(String, u64)>, + selected_folder: String, + ) -> Result<(), Box> { + for (folder, _) in folder_data { + if let Err(e) = self.generate_single_description(folder.clone(), selected_folder.clone()).await { + logger::log_error(&format!("处理文件夹 {} 时发生错误: {}", folder, e)); + continue; + } + } + Ok(()) + } + + // 保存配置并通知UI更新 + fn save_config_and_notify( + &self, + selected_folder: &str, + folder_name: &str, + description: &str, + ) -> Result<(), Box> { + if let Ok(config_path) = AIConfig::get_config_path() { + match self.config.save_to_file(config_path.to_str().unwrap()) { + Ok(_) => { + logger::log_info("配置文件保存成功"); + // 发送更新消息到 UI + if let Some(tx) = &self.tx { + let _ = tx.send(( + selected_folder.to_string(), + folder_name.to_string(), + description.to_string(), + )); + } + Ok(()) + } + Err(e) => { + logger::log_error(&format!("保存配置失败: {}", e)); + Err(e.into()) + } + } + } else { + Err("无法获取配置文件路径".into()) + } + } + + // 测试API连接 + pub async fn test_connection(&self) -> Result<(), Box> { + self.client.test_connection().await + } } \ No newline at end of file From 8a8ac1834477584d2bd2812c00ad74701f619031 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:05:04 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E7=A7=BB=E5=8A=A8ai=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=AE=9E=E7=8E=B0=E5=88=B0ai=5Fconfig.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui.rs | 238 +++++++++++++++++++----------------------------------- 1 file changed, 81 insertions(+), 157 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 80ce1f0..a73446e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,6 @@ use crate::about; -use crate::ai_config::{AIConfig, AIClient}; +use crate::ai_config::{AIConfig, AIHandler}; use crate::confirmation; -use crate::delete; use crate::ignore; use crate::logger; use crate::move_module; @@ -11,38 +10,47 @@ use crate::utils; use crate::yaml_loader::{load_folder_descriptions, FolderDescriptions}; use eframe::egui::{self, Grid, ScrollArea}; use std::collections::HashSet; +use std::sync::{Arc, Mutex}; use std::sync::mpsc::{Receiver, Sender}; pub struct AppDataCleaner { + // 基础字段 is_scanning: bool, current_folder: Option, folder_data: Vec<(String, u64)>, - show_ai_config_window: bool, // 新增字段,用于ai配置 - ai_url: String, // 新增字段,用于ai配置 - ai_api_key: String, // 新增字段,用于ai配置 - ai_model: String, // 新增字段,用于ai配置 - show_about_window: bool, // 确保字段存在 - confirm_delete: Option<(String, bool)>, // 保存要确认删除的文件夹状态 - selected_appdata_folder: String, // 新增字段 + selected_appdata_folder: String, tx: Option>, rx: Option>, - is_logging_enabled: bool, // 控制日志是否启用 - previous_logging_state: bool, // 记录上一次日志启用状态 - ignored_folders: HashSet, // 忽略文件夹集合 - move_module: move_module::MoveModule, // 移动模块实例 + total_size: u64, + + // 界面状态字段 + show_about_window: bool, + show_ai_config_window: bool, // AI配置窗口显示状态 + show_prompt_editor: bool, // Prompt编辑器显示状态 + confirm_delete: Option<(String, bool)>, + status: Option, // 状态信息 + + // 日志相关字段 + is_logging_enabled: bool, + previous_logging_state: bool, + + // 排序相关字段 + sort_criterion: Option, // 排序标准:"name"或"size" + sort_order: Option, // 排序顺序:"asc"或"desc" + + // 文件夹描述相关 folder_descriptions: Option, - yaml_error_logged: bool, // 新增字段,用于标记是否已经记录过错误 - status: Option, // 添加 status 字段 - sort_criterion: Option, // 新增字段,排序标准 "name" 或 "size" - sort_order: Option, // 新增字段,排序顺序 "asc" 或 "desc" - total_size: u64, // 新增字段,总大小 + yaml_error_logged: bool, + ignored_folders: HashSet, + + // 移动模块 + move_module: move_module::MoveModule, + + // AI相关配置 ai_config: AIConfig, - ai_retry_attempts: u32, - ai_retry_delay: u32, - show_prompt_editor: bool, - ai_client: Option, // 在 AppDataCleaner 结构体中添加 ai_client 字段 - ai_tx: Option>, // (folder_type, folder_name, description) + ai_tx: Option>, ai_rx: Option>, + ai_handler: Arc>, // 使用 Arc> 包装 AIHandler } impl Default for AppDataCleaner { @@ -50,7 +58,7 @@ impl Default for AppDataCleaner { let (tx, rx) = std::sync::mpsc::channel(); let (ai_tx, ai_rx) = std::sync::mpsc::channel(); - // 尝试加载配置文件 + // 加载AI配置 let ai_config = match AIConfig::load_from_file("folders_description.yaml") { Ok(config) => { logger::log_info("已成功加载AI配置文件"); @@ -62,38 +70,48 @@ impl Default for AppDataCleaner { } }; - // 先保存重试配置的值 - let attempts = ai_config.retry.attempts; - let delay = ai_config.retry.delay; + // 创建 AIHandler 并包装在 Arc> 中 + let ai_handler = Arc::new(Mutex::new(AIHandler::new( + ai_config.clone(), + Some(ai_tx.clone()) + ))); Self { + // 基础字段初始化 is_scanning: false, current_folder: None, folder_data: vec![], - show_ai_config_window: false, - ai_url: String::new(), - ai_api_key: String::new(), - ai_model: String::new(), - show_about_window: false, - confirm_delete: None, selected_appdata_folder: "Roaming".to_string(), tx: Some(tx), rx: Some(rx), + total_size: 0, + + // 界面状态初始化 + show_about_window: false, + show_ai_config_window: false, + show_prompt_editor: false, + confirm_delete: None, + status: Some("未扫描".to_string()), + + // 日志相关初始化 is_logging_enabled: false, previous_logging_state: false, - ignored_folders: ignore::load_ignored_folders(), - move_module: Default::default(), - folder_descriptions: None, - yaml_error_logged: false, - status: Some("未扫描".to_string()), + + // 排序相关初始化 sort_criterion: None, sort_order: None, - total_size: 0, - ai_config, // 移动 ai_config - ai_retry_attempts: attempts, // 使用保存的值 - ai_retry_delay: delay, // 使用保存的值 - show_prompt_editor: false, - ai_client: None, // 初始化 ai_client 字段 + + // 文件夹描述相关初始化 + folder_descriptions: None, + yaml_error_logged: false, + ignored_folders: ignore::load_ignored_folders(), + + // 移动模块初始化 + move_module: Default::default(), + + // AI相关初始化 + ai_handler, + ai_config, ai_tx: Some(ai_tx), ai_rx: Some(ai_rx), } @@ -166,69 +184,16 @@ impl eframe::App for AppDataCleaner { if ui.button("一键生成所有描述").clicked() { let folder_data = self.folder_data.clone(); let selected_folder = self.selected_appdata_folder.clone(); - let mut ai_config = self.ai_config.clone(); - let ai_tx = self.ai_tx.clone(); + let handler = self.ai_handler.clone(); // 克隆Arc> - // 更新状态提示正在处理 self.status = Some("正在生成描述...".to_string()); - // 创建一个新的客户端实例 - let client = AIClient::new(ai_config.clone()); - - // 创建一个后台任务 std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - for (folder, _) in folder_data { - logger::log_info(&format!("开始为 {} 生成描述", folder)); - - match client.get_folder_description(&selected_folder, &folder).await { - Ok(description) => { - logger::log_info(&format!( - "成功生成描述 - {}/{}: {}", - selected_folder, - folder, - description - )); - - // 更新配置 - match selected_folder.as_str() { - "Local" => ai_config.Local.insert(folder.clone(), description.clone()), - "LocalLow" => ai_config.LocalLow.insert(folder.clone(), description.clone()), - "Roaming" => ai_config.Roaming.insert(folder.clone(), description.clone()), - _ => None, - }; - - // 立即保存配置到文件 - if let Ok(config_path) = AIConfig::get_config_path() { - match ai_config.save_to_file(config_path.to_str().unwrap()) { - Ok(_) => { - logger::log_info("配置文件保存成功"); - // 发送更新消息到 UI - if let Some(tx) = &ai_tx { - let _ = tx.send((selected_folder.clone(), folder.clone(), description)); - } - } - Err(e) => logger::log_error(&format!("保存配置失败: {}", e)), - } - } - } - Err(e) => { - logger::log_error(&format!( - "生成描述失败 {}/{}: {}", - selected_folder, - folder, - e - )); - } - } - } - - // 保存更新后的配置 - if let Ok(config_path) = AIConfig::get_config_path() { - match ai_config.save_to_file(config_path.to_str().unwrap()) { - Ok(_) => logger::log_info("配置文件保存成功"), - Err(e) => logger::log_error(&format!("保存配置失败: {}", e)), + if let Ok(mut handler) = handler.lock() { + if let Err(e) = handler.generate_all_descriptions(folder_data, selected_folder).await { + logger::log_error(&format!("批量生成描述失败: {}", e)); } } }); @@ -392,59 +357,16 @@ impl eframe::App for AppDataCleaner { if ui.button("生成描述").clicked() { let folder_name = folder.clone(); let selected_folder = self.selected_appdata_folder.clone(); - let mut ai_config = self.ai_config.clone(); - let ai_tx = self.ai_tx.clone(); + let handler = self.ai_handler.clone(); // 克隆Arc> - // 更新状态提示正在处理 self.status = Some(format!("正在为 {} 生成描述...", folder_name)); - // 创建一个新的客户端实例 - let client = AIClient::new(ai_config.clone()); - - // 创建一个后台任务 std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - logger::log_info(&format!("开始为 {} 生成描述", folder_name)); - - match client.get_folder_description(&selected_folder, &folder_name).await { - Ok(description) => { - logger::log_info(&format!( - "成功生成描述 - {}/{}: {}", - selected_folder, - folder_name, - description - )); - - // 更新配置 - match selected_folder.as_str() { - "Local" => { ai_config.Local.insert(folder_name.clone(), description.clone()); } - "LocalLow" => { ai_config.LocalLow.insert(folder_name.clone(), description.clone()); } - "Roaming" => { ai_config.Roaming.insert(folder_name.clone(), description.clone()); } - _ => {} - }; - - // 立即保存配置到文件 - if let Ok(config_path) = AIConfig::get_config_path() { - match ai_config.save_to_file(config_path.to_str().unwrap()) { - Ok(_) => { - logger::log_info("配置文件保存成功"); - // 发送更新消息到 UI - if let Some(tx) = &ai_tx { - let _ = tx.send((selected_folder.clone(), folder_name.clone(), description)); - } - } - Err(e) => logger::log_error(&format!("保存配置失败: {}", e)), - } - } - } - Err(e) => { - logger::log_error(&format!( - "生成描述失败 {}/{}: {}", - selected_folder, - folder_name, - e - )); + if let Ok(mut handler) = handler.lock() { + if let Err(e) = handler.generate_single_description(folder_name.clone(), selected_folder).await { + logger::log_error(&format!("生成描述失败: {}", e)); } } }); @@ -583,19 +505,21 @@ impl eframe::App for AppDataCleaner { } if ui.button("测试连接").clicked() { - let client = AIClient::new(self.ai_config.clone()); + let handler = self.ai_handler.clone(); // 克隆Arc> tokio::runtime::Runtime::new() .unwrap() .block_on(async { - match client.test_connection().await { - Ok(_) => { - logger::log_info("AI连接测试成功"); - self.status = Some("AI连接测试成功".to_string()); - } - Err(err) => { - logger::log_error(&format!("AI连接测试失败: {}", err)); - self.status = Some(format!("AI连接测试失败: {}", err)); + if let Ok(handler) = handler.lock() { + match handler.test_connection().await { + Ok(_) => { + logger::log_info("AI连接测试成功"); + self.status = Some("AI连接测试成功".to_string()); + } + Err(err) => { + logger::log_error(&format!("AI连接测试失败: {}", err)); + self.status = Some(format!("AI连接测试失败: {}", err)); + } } } }); From e77b3c3197c1d5620682f98c316fa642b236e734 Mon Sep 17 00:00:00 2001 From: Xch13 <149986830+Xch13@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:03:57 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E5=B0=86ai=E9=80=BB=E8=BE=91=E6=8B=86?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai_state.rs | 0 src/ai_ui.rs | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/ai_state.rs create mode 100644 src/ai_ui.rs diff --git a/src/ai_state.rs b/src/ai_state.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ai_ui.rs b/src/ai_ui.rs new file mode 100644 index 0000000..e69de29