From 6b94aaf260753bebf9dbf095fad9a820f6d0d6a3 Mon Sep 17 00:00:00 2001 From: imeepos Date: Thu, 7 Aug 2025 17:13:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 160 +++++++++++ .../workflow_template_repository.rs | 265 ++++++++++++------ .../canvas/nodes/FolderUploadNode.tsx | 2 +- .../canvas/nodes/TextDisplayNode.tsx | 4 +- .../components/workflow/DataExportManager.tsx | 20 +- .../workflow/EnvironmentConfigurator.tsx | 16 +- apps/desktop/src/hooks/useNodeProcessing.ts | 2 +- apps/desktop/src/pages/WorkflowPage.tsx | 39 ++- apps/desktop/src/types/canvas.ts | 6 + apps/desktop/tsconfig.json | 4 +- 10 files changed, 400 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1267d5..e8a0b9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -321,6 +332,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -430,6 +456,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -502,6 +548,11 @@ name = "cc" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] [[package]] name = "cesu8" @@ -557,6 +608,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -582,6 +643,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -2084,6 +2151,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "io-uring" version = "0.7.9" @@ -2181,6 +2257,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.2" @@ -2466,6 +2552,7 @@ version = "0.2.1" dependencies = [ "anyhow", "base64 0.22.1", + "bincode", "chrono", "dirs 5.0.1", "futures-util", @@ -2504,6 +2591,7 @@ dependencies = [ "urlencoding", "uuid", "winapi", + "zip", ] [[package]] @@ -3057,12 +3145,35 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -6474,6 +6585,55 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/apps/desktop/src-tauri/src/data/repositories/workflow_template_repository.rs b/apps/desktop/src-tauri/src/data/repositories/workflow_template_repository.rs index 2432e79..6bfa250 100644 --- a/apps/desktop/src-tauri/src/data/repositories/workflow_template_repository.rs +++ b/apps/desktop/src-tauri/src/data/repositories/workflow_template_repository.rs @@ -3,6 +3,7 @@ use crate::data::models::workflow_template::{ UpdateWorkflowTemplateRequest, WorkflowTemplateFilter }; use crate::infrastructure::database::Database; +use crate::infrastructure::tolerant_json_parser::{CachedTolerantJsonParser, ParserConfig, RecoveryStrategy}; use anyhow::{Result, anyhow}; use chrono::{DateTime, Utc}; use rusqlite::Row; @@ -14,12 +15,123 @@ use tracing::{debug, info, error}; #[derive(Clone)] pub struct WorkflowTemplateRepository { database: Arc, + json_parser: Arc, } impl WorkflowTemplateRepository { /// 创建新的工作流模板仓库实例 pub fn new(database: Arc) -> Self { - Self { database } + // 创建带缓存的容错JSON解析器,配置为适合工作流模板的解析 + let json_parser = CachedTolerantJsonParser::new(Some(ParserConfig { + max_text_length: 10_000_000, // 10MB限制,适合大型工作流 + enable_comments: true, + enable_unquoted_keys: true, + enable_trailing_commas: true, + timeout_ms: 30000, // 30秒超时 + recovery_strategies: vec![ + RecoveryStrategy::StandardJson, + RecoveryStrategy::ManualFix, + RecoveryStrategy::RegexExtract, + RecoveryStrategy::PartialParse, + ], + }), Some(1000)).expect("Failed to create JSON parser"); + + Self { + database, + json_parser: Arc::new(json_parser), + } + } + + /// 安全地解析JSON字段的辅助函数,使用容错JSON解析器 + fn parse_json_field(&self, row: &Row, field_name: &str) -> rusqlite::Result { + match row.get::<_, Option>(field_name) { + Ok(Some(json_str)) => { + if json_str.trim().is_empty() { + Ok(serde_json::Value::Object(serde_json::Map::new())) + } else { + // 使用容错JSON解析器 + match self.json_parser.parse(&json_str) { + Ok((parsed_value, _stats)) => { + debug!("Successfully parsed {} using tolerant parser", field_name); + Ok(parsed_value) + }, + Err(e) => { + error!("Tolerant JSON parsing failed for {}: '{}', error: {}", field_name, json_str, e); + // 如果容错解析也失败,返回空对象而不是错误 + Ok(serde_json::Value::Object(serde_json::Map::new())) + } + } + } + }, + Ok(None) => Ok(serde_json::Value::Object(serde_json::Map::new())), + Err(e) => { + error!("Error getting {}: {}", field_name, e); + Err(e) + } + } + } + + /// 安全地解析可选JSON字段的辅助函数,使用容错JSON解析器 + fn parse_optional_json_field(&self, row: &Row, field_name: &str) -> rusqlite::Result> { + match row.get::<_, Option>(field_name) { + Ok(Some(json_str)) => { + if json_str.trim().is_empty() { + Ok(Some(serde_json::Value::Object(serde_json::Map::new()))) + } else { + // 使用容错JSON解析器 + match self.json_parser.parse(&json_str) { + Ok((parsed_value, _stats)) => { + debug!("Successfully parsed optional {} using tolerant parser", field_name); + Ok(Some(parsed_value)) + }, + Err(e) => { + error!("Tolerant JSON parsing failed for optional {}: '{}', error: {}", field_name, json_str, e); + // 如果容错解析也失败,返回空对象而不是错误 + Ok(Some(serde_json::Value::Object(serde_json::Map::new()))) + } + } + } + }, + Ok(None) => Ok(None), + Err(e) => { + error!("Error getting {}: {}", field_name, e); + Err(e) + } + } + } + + /// 解析多种时间戳格式的辅助函数 + fn parse_datetime(&self, datetime_str: &str) -> Result, Box> { + use chrono::{DateTime, NaiveDateTime, Utc}; + + // 尝试多种时间格式 + + // 1. 尝试 RFC3339 格式 (ISO 8601) + if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) { + return Ok(dt.with_timezone(&Utc)); + } + + // 2. 尝试常见的数据库格式: "YYYY-MM-DD HH:MM:SS" + if let Ok(naive_dt) = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S") { + return Ok(DateTime::from_naive_utc_and_offset(naive_dt, Utc)); + } + + // 3. 尝试带毫秒的格式: "YYYY-MM-DD HH:MM:SS.fff" + if let Ok(naive_dt) = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f") { + return Ok(DateTime::from_naive_utc_and_offset(naive_dt, Utc)); + } + + // 4. 尝试 ISO 格式但没有时区: "YYYY-MM-DDTHH:MM:SS" + if let Ok(naive_dt) = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%dT%H:%M:%S") { + return Ok(DateTime::from_naive_utc_and_offset(naive_dt, Utc)); + } + + // 5. 尝试 ISO 格式带毫秒但没有时区: "YYYY-MM-DDTHH:MM:SS.fff" + if let Ok(naive_dt) = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%dT%H:%M:%S%.f") { + return Ok(DateTime::from_naive_utc_and_offset(naive_dt, Utc)); + } + + Err(format!("Unable to parse datetime: '{}'", datetime_str).into()) } /// 创建工作流模板 @@ -139,8 +251,9 @@ impl WorkflowTemplateRepository { let mut stmt = conn.prepare(&sql) .map_err(|e| anyhow!("准备查询语句失败: {}", e))?; - let template_iter = stmt.query_map(rusqlite::params_from_iter(params.iter()), |row| self.row_to_template(row)) - .map_err(|e| anyhow!("执行查询失败: {}", e))?; + let template_iter = stmt.query_map(rusqlite::params_from_iter(params.iter()), |row| { + self.row_to_template(row) + }).map_err(|e| anyhow!("执行查询失败: {}", e))?; let mut templates = Vec::new(); for template in template_iter { @@ -467,104 +580,86 @@ impl WorkflowTemplateRepository { /// 将数据库行转换为WorkflowTemplate对象 fn row_to_template(&self, row: &Row) -> rusqlite::Result { + // 解析基本字段 let id: Option = Some(row.get("id")?); - let name: String = row.get("name")?; - let base_name: String = row.get("base_name")?; - let version: String = row.get("version")?; - // 解析工作流类型 - let type_str: String = row.get("type")?; + let name: String = row.get::<_, Option>("name")?.unwrap_or_default(); + if name.is_empty() { + error!("Template name is empty or null"); + return Err(rusqlite::Error::InvalidColumnType(0, "name".to_string(), rusqlite::types::Type::Text)); + } + + let base_name: String = row.get::<_, Option>("base_name")?.unwrap_or_default(); + let version: String = row.get::<_, Option>("version")?.unwrap_or("1.0.0".to_string()); + let type_str: String = row.get::<_, Option>("type")?.unwrap_or("image_generation".to_string()); let workflow_type: WorkflowType = WorkflowType::from(type_str); - let description: Option = row.get("description")?; - // 解析JSON字段,处理空字符串的情况 - let comfyui_workflow_json_str: String = row.get("comfyui_workflow_json")?; - let comfyui_workflow_json: serde_json::Value = if comfyui_workflow_json_str.trim().is_empty() { - serde_json::Value::Object(serde_json::Map::new()) - } else { - serde_json::from_str(&comfyui_workflow_json_str) - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))? - }; - - let ui_config_json_str: String = row.get("ui_config_json")?; - let ui_config_json: serde_json::Value = if ui_config_json_str.trim().is_empty() { - serde_json::Value::Object(serde_json::Map::new()) - } else { - serde_json::from_str(&ui_config_json_str) - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))? - }; - - let execution_config_json: Option = row.get::<_, Option>("execution_config_json")? - .map(|s| { - if s.trim().is_empty() { - Ok(serde_json::Value::Object(serde_json::Map::new())) - } else { - serde_json::from_str(&s) - } - }) - .transpose() - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))?; - - let input_schema_json: Option = row.get::<_, Option>("input_schema_json")? - .map(|s| { - if s.trim().is_empty() { - Ok(serde_json::Value::Object(serde_json::Map::new())) - } else { - serde_json::from_str(&s) - } - }) - .transpose() - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))?; - - let output_schema_json: Option = row.get::<_, Option>("output_schema_json")? - .map(|s| { - if s.trim().is_empty() { - Ok(serde_json::Value::Object(serde_json::Map::new())) - } else { - serde_json::from_str(&s) - } - }) - .transpose() - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))?; + // 解析JSON字段 + let comfyui_workflow_json = self.parse_json_field(row, "comfyui_workflow_json")?; + let ui_config_json = self.parse_json_field(row, "ui_config_json")?; + let execution_config_json = self.parse_optional_json_field(row, "execution_config_json")?; + let input_schema_json = self.parse_optional_json_field(row, "input_schema_json")?; + let output_schema_json = self.parse_optional_json_field(row, "output_schema_json")?; + // 解析布尔字段 let is_active: bool = row.get("is_active")?; let is_published: bool = row.get("is_published")?; - let tags: Option> = row.get::<_, Option>("tags")? - .map(|s| serde_json::from_str(&s)) - .transpose() - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))?; + // 解析tags字段 + let tags: Option> = match row.get::<_, Option>("tags")? { + Some(tags_str) => { + if tags_str.trim().is_empty() { + None + } else { + // 使用容错JSON解析器解析tags + match self.json_parser.parse(&tags_str) { + Ok((parsed_value, _stats)) => { + match parsed_value.as_array() { + Some(arr) => { + let string_tags: Vec = arr.iter() + .map(|v| v.as_str().unwrap_or("").to_string()) + .filter(|s| !s.is_empty()) + .collect(); + Some(string_tags) + }, + None => { + error!("Tags field is not an array: {}", tags_str); + None + } + } + }, + Err(e) => { + error!("Failed to parse tags field: '{}', error: {}", tags_str, e); + None + } + } + } + }, + None => None + }; let category: Option = row.get("category")?; let author: Option = row.get("author")?; - // 解析时间戳 + // 解析时间戳 - 处理多种时间格式 let created_at_str: String = row.get("created_at")?; - let created_at = DateTime::parse_from_rfc3339(&created_at_str) - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))? - .with_timezone(&Utc); + let created_at = self.parse_datetime(&created_at_str) + .map_err(|e| { + error!("解析 created_at 时间戳失败: '{}', error: {}", created_at_str, e); + rusqlite::Error::FromSqlConversionFailure( + 0, rusqlite::types::Type::Text, e + ) + })?; let updated_at_str: String = row.get("updated_at")?; - let updated_at = DateTime::parse_from_rfc3339(&updated_at_str) - .map_err(|e| rusqlite::Error::FromSqlConversionFailure( - 0, rusqlite::types::Type::Text, Box::new(e) - ))? - .with_timezone(&Utc); + let updated_at = self.parse_datetime(&updated_at_str) + .map_err(|e| { + error!("解析 updated_at 时间戳失败: '{}', error: {}", updated_at_str, e); + rusqlite::Error::FromSqlConversionFailure( + 0, rusqlite::types::Type::Text, e + ) + })?; Ok(WorkflowTemplate { id, diff --git a/apps/desktop/src/components/canvas/nodes/FolderUploadNode.tsx b/apps/desktop/src/components/canvas/nodes/FolderUploadNode.tsx index b2fc588..8fdfe30 100644 --- a/apps/desktop/src/components/canvas/nodes/FolderUploadNode.tsx +++ b/apps/desktop/src/components/canvas/nodes/FolderUploadNode.tsx @@ -71,7 +71,7 @@ export const FolderUploadNode: React.FC = ({ id, data }) diff --git a/apps/desktop/src/components/canvas/nodes/TextDisplayNode.tsx b/apps/desktop/src/components/canvas/nodes/TextDisplayNode.tsx index b079635..410b712 100644 --- a/apps/desktop/src/components/canvas/nodes/TextDisplayNode.tsx +++ b/apps/desktop/src/components/canvas/nodes/TextDisplayNode.tsx @@ -37,8 +37,8 @@ export const TextDisplayNode: React.FC = ({ id, data }) => {data.result && (
{data.result.length} 字符 -