diff --git a/Cargo.toml b/Cargo.toml index 50a8b84..35a7f9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,12 @@ 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" +reqwest = { version = "0.11", features = ["json"] } diff --git a/src/ai_config.rs b/src/ai_config.rs new file mode 100644 index 0000000..4872a13 --- /dev/null +++ b/src/ai_config.rs @@ -0,0 +1,410 @@ +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 +#[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, Clone)] +pub struct ModelConfig { + pub url: String, + pub api_key: String, + pub model: String, + pub prompt: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RetryConfig { + pub attempts: u32, + pub delay: u32, +} + +impl Default for AIConfig { + fn default() -> Self { + Self { + name: String::new(), + model: ModelConfig { + url: "https://open.bigmodel.cn/api/paas/v4/chat/completions".to_string(), + api_key: "your_api_key_here".to_string(), + 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 { + attempts: 3, + delay: 20, + }, + Local: HashMap::new(), + LocalLow: HashMap::new(), + Roaming: HashMap::new(), + } + } +} + +impl AIConfig { + pub fn new() -> Self { + Self::default() + } + + // 简化路径处理,直接使用固定路径 + pub fn get_config_path() -> Result> { + Ok(std::path::PathBuf::from("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) + } + + pub fn save_to_file(&self, path: &str) -> Result<(), Box> { + // 序列化配置 + let content = serde_yaml::to_string(self)?; + + // 写入文件 + std::fs::write(path, content)?; + + logger::log_info(&format!("配置文件已保存到: {}", path)); + + 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; + } + + // 获取正确的配置文件路径 + let config_path = Self::get_config_path()?; + config.save_to_file(config_path.to_str().unwrap())?; + + Ok(()) + } + + 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(()) + } +} + +// API 请求相关结构体 +#[derive(Debug, Serialize, Deserialize)] +pub struct Message { + pub role: String, + pub content: String, +} + +#[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(), + } + } + + // 发送请求到 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) => { + 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; + } + } + } + } + + // 单次请求实现 + 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(), + }; + + 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()) + } + } + + // 测试连接 + 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(), + }; + + 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() { + Ok(()) + } else { + 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 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 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 6a8b663..a73446e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,60 +1,119 @@ use crate::about; +use crate::ai_config::{AIConfig, AIHandler}; 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; 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_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_tx: Option>, + ai_rx: Option>, + ai_handler: Arc>, // 使用 Arc> 包装 AIHandler } impl Default for AppDataCleaner { fn default() -> Self { 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配置文件"); + config + } + Err(_) => { + logger::log_info("未找到配置文件,使用默认配置"); + AIConfig::default() + } + }; + + // 创建 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_about_window: false, // 默认值 - confirm_delete: None, // 初始化为 None - selected_appdata_folder: "Roaming".to_string(), // 默认值为 Roaming + selected_appdata_folder: "Roaming".to_string(), tx: Some(tx), rx: Some(rx), - is_logging_enabled: false, // 默认禁用日志 - previous_logging_state: false, // 初始时假定日志系统未启用 + 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, + + // 排序相关初始化 + sort_criterion: None, + sort_order: None, + + // 文件夹描述相关初始化 + folder_descriptions: None, + yaml_error_logged: 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 + + // AI相关初始化 + ai_handler, + ai_config, + ai_tx: Some(ai_tx), + ai_rx: Some(ai_rx), } } } @@ -113,10 +172,34 @@ 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(); + } + if ui.button("一键生成所有描述").clicked() { + let folder_data = self.folder_data.clone(); + let selected_folder = self.selected_appdata_folder.clone(); + let handler = self.ai_handler.clone(); // 克隆Arc> + + self.status = Some("正在生成描述...".to_string()); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + 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)); + } + } + }); + }); + } + }); ui.separator(); ui.checkbox(&mut self.is_logging_enabled, "启用日志"); @@ -271,6 +354,24 @@ impl eframe::App for AppDataCleaner { } } } + if ui.button("生成描述").clicked() { + let folder_name = folder.clone(); + let selected_folder = self.selected_appdata_folder.clone(); + let handler = self.ai_handler.clone(); // 克隆Arc> + + self.status = Some(format!("正在为 {} 生成描述...", folder_name)); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + 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)); + } + } + }); + }); + } ui.end_row(); } }); @@ -282,7 +383,220 @@ 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) + .min_width(400.0) // 添加最小宽度 + .min_height(500.0) // 添加最小高度 + .show(ctx, |ui| { + ui.heading("AI配置生成器"); + + // 基本配置 + 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)); + }); + + 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)); + }); + }); + + // 重试配置组 + 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() { + 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() { + let handler = self.ai_handler.clone(); // 克隆Arc> + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async { + 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)); + } + } + } + }); + } + + if ui.button("重置默认值").clicked() { + self.ai_config = AIConfig::default(); + } + + if ui.button("关闭").clicked() { + self.show_ai_config_window = false; + } + }); + }); + } + + // 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; + } + }); + }); + } + + // 在主循环中处理接收到的更新 + 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); } -} +} \ No newline at end of file