diff --git a/Cargo.lock b/Cargo.lock index cb1fa74..d1267d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2501,6 +2501,7 @@ dependencies = [ "tree-sitter", "tree-sitter-json", "url", + "urlencoding", "uuid", "winapi", ] @@ -5301,6 +5302,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 7525695..20e102e 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -55,6 +55,7 @@ hmac = "0.12.1" sha2 = "0.10.9" hex = "0.4.3" url = "2.5.4" +urlencoding = "2.1" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["sysinfoapi"] } diff --git a/apps/desktop/src-tauri/src/app_state.rs b/apps/desktop/src-tauri/src/app_state.rs index 3f7d72c..d107e03 100644 --- a/apps/desktop/src-tauri/src/app_state.rs +++ b/apps/desktop/src-tauri/src/app_state.rs @@ -11,6 +11,7 @@ use crate::infrastructure::database::Database; use crate::infrastructure::performance::PerformanceMonitor; use crate::infrastructure::event_bus::EventBusManager; use crate::infrastructure::bowong_text_video_agent_service::BowongTextVideoAgentService; +use crate::infrastructure::comfyui_service::ComfyuiService; /// 应用全局状态管理 @@ -28,7 +29,7 @@ pub struct AppState { pub performance_monitor: Mutex, pub event_bus_manager: Arc, pub bowong_text_video_agent_service: Mutex>, - + pub comfyui_service: Mutex>, } impl AppState { @@ -46,7 +47,7 @@ impl AppState { performance_monitor: Mutex::new(PerformanceMonitor::new()), event_bus_manager: Arc::new(EventBusManager::new()), bowong_text_video_agent_service: Mutex::new(None), - + comfyui_service: Mutex::new(None), } } @@ -223,7 +224,7 @@ impl AppState { performance_monitor: Mutex::new(PerformanceMonitor::new()), event_bus_manager: Arc::new(EventBusManager::new()), bowong_text_video_agent_service: Mutex::new(None), - + comfyui_service: Mutex::new(None), }; // 不直接存储database,而是在需要时返回传入的database state @@ -248,9 +249,44 @@ impl AppState { .map_err(|e| anyhow::anyhow!("Failed to acquire lock for BowongTextVideoAgent service: {}", e)) } + /// 初始化 ComfyUI 服务 + pub fn initialize_comfyui_service(&self, config: crate::data::models::comfyui::ComfyuiConfig) -> anyhow::Result<()> { + let service = ComfyuiService::new(config)?; + let mut comfyui_service = self.comfyui_service.lock() + .map_err(|e| anyhow::anyhow!("Failed to acquire lock for ComfyUI service: {}", e))?; + *comfyui_service = Some(service); + println!("✅ ComfyUI 服务初始化成功"); + Ok(()) + } + + /// 获取 ComfyUI 服务的引用 + pub fn get_comfyui_service(&self) -> Option { + match self.comfyui_service.lock() { + Ok(guard) => guard.clone(), + Err(e) => { + eprintln!("Failed to acquire lock for ComfyUI service: {}", e); + None + } + } + } + + /// 更新 ComfyUI 服务配置 + pub async fn update_comfyui_config(&self, config: crate::data::models::comfyui::ComfyuiConfig) -> anyhow::Result<()> { + // 创建新的服务实例 + let new_service = ComfyuiService::new(config)?; + + // 更新服务 + let mut comfyui_service = self.comfyui_service.lock() + .map_err(|e| anyhow::anyhow!("Failed to acquire lock for ComfyUI service: {}", e))?; + + *comfyui_service = Some(new_service); + + println!("✅ ComfyUI 服务配置更新成功"); + Ok(()) + } } impl Default for AppState { diff --git a/apps/desktop/src-tauri/src/data/models/comfyui.rs b/apps/desktop/src-tauri/src/data/models/comfyui.rs new file mode 100644 index 0000000..2d69236 --- /dev/null +++ b/apps/desktop/src-tauri/src/data/models/comfyui.rs @@ -0,0 +1,239 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; + +/// ComfyUI API 服务配置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComfyuiConfig { + /// API 基础 URL + pub base_url: String, + /// 请求超时时间(秒) + pub timeout: Option, + /// 重试次数 + pub retry_attempts: Option, + /// 是否启用缓存 + pub enable_cache: Option, + /// 最大并发数 + pub max_concurrency: Option, +} + +impl Default for ComfyuiConfig { + fn default() -> Self { + Self { + base_url: "https://bowongai-dev--waas-demo-fastapi-webapp.modal.run".to_string(), + timeout: Some(30), + retry_attempts: Some(3), + enable_cache: Some(true), + max_concurrency: Some(8), + } + } +} + +/// 工作流对象 - 用于 GET /api/workflow 响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Workflow { + /// 工作流的完整唯一名称,例如 'my_workflow [20250101120000]' + pub name: String, + /// 工作流的基础名称 + pub base_name: Option, + /// 工作流版本 + pub version: Option, + /// 工作流创建时间 + pub created_at: Option>, + /// 工作流更新时间 + pub updated_at: Option>, + /// 工作流描述 + pub description: Option, + /// 工作流配置数据 + #[serde(flatten)] + pub additional_properties: HashMap, +} + +/// 发布工作流请求 - 用于 POST /api/workflow +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublishWorkflowRequest { + /// 工作流名称 + pub name: String, + /// 工作流配置数据 + pub workflow_data: serde_json::Value, + /// 工作流描述 + pub description: Option, + /// 工作流版本 + pub version: Option, +} + +/// 发布工作流响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublishWorkflowResponse { + /// 操作是否成功 + pub success: bool, + /// 响应消息 + pub message: Option, + /// 创建的工作流信息 + pub workflow: Option, +} + +/// 删除工作流响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteWorkflowResponse { + /// 操作是否成功 + pub success: bool, + /// 响应消息 + pub message: Option, + /// 删除的工作流名称 + pub deleted_workflow: Option, +} + +/// 执行工作流请求 - 用于 POST /api/run/ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExecuteWorkflowRequest { + /// 工作流基础名称 + pub base_name: String, + /// 工作流版本(可选) + pub version: Option, + /// 请求数据 + pub request_data: serde_json::Value, +} + +/// 执行工作流响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExecuteWorkflowResponse { + /// 执行任务ID + pub task_id: Option, + /// 执行状态 + pub status: Option, + /// 响应消息 + pub message: Option, + /// 执行结果数据 + pub result: Option, + /// 错误信息 + pub error: Option, +} + +/// 获取工作流规范请求 - 用于 GET /api/spec/ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetWorkflowSpecRequest { + /// 工作流基础名称 + pub base_name: String, + /// 工作流版本(可选) + pub version: Option, +} + +/// 获取工作流规范响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetWorkflowSpecResponse { + /// 工作流规范数据 + pub spec: serde_json::Value, + /// 工作流名称 + pub name: Option, + /// 工作流版本 + pub version: Option, + /// 规范描述 + pub description: Option, +} + +/// 服务器队列详情 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerQueueDetails { + /// 正在运行的任务数量 + pub running_count: i32, + /// 等待中的任务数量 + pub pending_count: i32, +} + +/// 服务器状态信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerStatus { + /// 服务器在配置列表中的索引 + pub server_index: i32, + /// HTTP URL + pub http_url: String, + /// WebSocket URL + pub ws_url: String, + /// 输入目录路径 + pub input_dir: String, + /// 输出目录路径 + pub output_dir: String, + /// 服务器是否可达 + pub is_reachable: bool, + /// 服务器是否空闲 + pub is_free: bool, + /// 队列详情 + pub queue_details: ServerQueueDetails, +} + +/// 文件详情 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileDetails { + /// 文件名 + pub name: String, + /// 文件大小(KB) + pub size_kb: f64, + /// 修改时间 + pub modified_at: DateTime, +} + +/// 服务器文件信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerFiles { + /// 服务器在配置列表中的索引 + pub server_index: i32, + /// HTTP URL + pub http_url: String, + /// 输入文件列表 + pub input_files: Vec, + /// 输出文件列表 + pub output_files: Vec, +} + +/// API 根响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiRootResponse { + /// API 使用指南 + pub guide: String, + /// API 版本 + pub version: Option, + /// 可用端点 + pub endpoints: Option>, +} + +/// HTTP 验证错误详情 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidationError { + /// 错误位置 + pub loc: Vec, + /// 错误消息 + pub msg: String, + /// 错误类型 + #[serde(rename = "type")] + pub error_type: String, +} + +/// HTTP 验证错误响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HTTPValidationError { + /// 错误详情列表 + pub detail: Vec, +} + +/// ComfyUI API 错误类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ComfyuiError { + /// 网络连接错误 + NetworkError { message: String }, + /// HTTP 错误 + HttpError { status: u16, message: String }, + /// 验证错误 + ValidationError { errors: Vec }, + /// 服务器内部错误 + ServerError { message: String }, + /// 工作流不存在 + WorkflowNotFound { workflow_name: String }, + /// 配置错误 + ConfigError { message: String }, + /// 超时错误 + TimeoutError { message: String }, + /// 未知错误 + UnknownError { message: String }, +} diff --git a/apps/desktop/src-tauri/src/data/models/mod.rs b/apps/desktop/src-tauri/src/data/models/mod.rs index 105674f..824305f 100644 --- a/apps/desktop/src-tauri/src/data/models/mod.rs +++ b/apps/desktop/src-tauri/src/data/models/mod.rs @@ -30,3 +30,4 @@ pub mod outfit_image; pub mod outfit_photo_generation; pub mod video_generation_record; pub mod hedra_lipsync_record; +pub mod comfyui; diff --git a/apps/desktop/src-tauri/src/infrastructure/comfyui_service.rs b/apps/desktop/src-tauri/src/infrastructure/comfyui_service.rs new file mode 100644 index 0000000..5c8262c --- /dev/null +++ b/apps/desktop/src-tauri/src/infrastructure/comfyui_service.rs @@ -0,0 +1,325 @@ +use crate::data::models::comfyui::*; +use anyhow::{anyhow, Result}; +use reqwest::{Client, Response}; +use serde_json::Value; +use std::time::Duration; +use tracing::{debug, error, info, warn}; + +/// ComfyUI API 服务 +/// +/// 提供对 ComfyUI Workflow Service & Management API 的完整封装 +/// 基于 OpenAPI 3.1.0 规范实现所有端点 +#[derive(Debug, Clone)] +pub struct ComfyuiService { + /// HTTP 客户端 + client: Client, + /// 服务配置 + config: ComfyuiConfig, +} + +impl ComfyuiService { + /// 创建新的 ComfyUI 服务实例 + pub fn new(config: ComfyuiConfig) -> Result { + let timeout = Duration::from_secs(config.timeout.unwrap_or(30)); + + let client = Client::builder() + .timeout(timeout) + .build() + .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?; + + info!("ComfyUI service initialized with base URL: {}", config.base_url); + + Ok(Self { client, config }) + } + + /// 获取所有工作流 + /// + /// GET /api/workflow + /// 返回所有已发布的工作流列表 + pub async fn get_all_workflows(&self) -> Result> { + let url = format!("{}/api/workflow", self.config.base_url.trim_end_matches('/')); + debug!("Getting all workflows from: {}", url); + + let response = self.execute_get_request(&url).await?; + let workflows: Vec = response.json().await + .map_err(|e| anyhow!("Failed to parse workflows response: {}", e))?; + + info!("Retrieved {} workflows", workflows.len()); + Ok(workflows) + } + + /// 发布工作流 + /// + /// POST /api/workflow + /// 发布新的工作流或更新现有工作流 + pub async fn publish_workflow(&self, request: PublishWorkflowRequest) -> Result { + let url = format!("{}/api/workflow", self.config.base_url.trim_end_matches('/')); + debug!("Publishing workflow '{}' to: {}", request.name, url); + + let response = self.execute_post_request(&url, &request).await?; + + // 根据 OpenAPI 规范,成功响应状态码是 201 + if response.status().as_u16() == 201 { + // 响应体可能为空,创建成功响应 + let workflow_response = PublishWorkflowResponse { + success: true, + message: Some("Workflow published successfully".to_string()), + workflow: None, // API 规范中响应体为空 + }; + info!("Workflow '{}' published successfully", request.name); + Ok(workflow_response) + } else { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + Err(anyhow!("Failed to publish workflow: HTTP {} - {}", status, error_text)) + } + } + + /// 删除工作流 + /// + /// DELETE /api/workflow/{workflow_name} + /// 删除指定的工作流 + pub async fn delete_workflow(&self, workflow_name: &str) -> Result { + let encoded_name = urlencoding::encode(workflow_name); + let url = format!("{}/api/workflow/{}", self.config.base_url.trim_end_matches('/'), encoded_name); + debug!("Deleting workflow '{}' from: {}", workflow_name, url); + + let response = self.execute_delete_request(&url).await?; + + if response.status().is_success() { + let delete_response = DeleteWorkflowResponse { + success: true, + message: Some("Workflow deleted successfully".to_string()), + deleted_workflow: Some(workflow_name.to_string()), + }; + info!("Workflow '{}' deleted successfully", workflow_name); + Ok(delete_response) + } else { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + Err(anyhow!("Failed to delete workflow: HTTP {} - {}", status, error_text)) + } + } + + /// 执行工作流 + /// + /// POST /api/run/ + /// 执行指定的工作流 + pub async fn execute_workflow(&self, request: ExecuteWorkflowRequest) -> Result { + let mut url = format!("{}/api/run/", self.config.base_url.trim_end_matches('/')); + + // 添加查询参数 + url.push_str(&format!("?base_name={}", urlencoding::encode(&request.base_name))); + if let Some(version) = &request.version { + url.push_str(&format!("&version={}", urlencoding::encode(version))); + } + + debug!("Executing workflow '{}' at: {}", request.base_name, url); + + let response = self.execute_post_request(&url, &request.request_data).await?; + + if response.status().is_success() { + // 尝试解析响应,如果失败则返回基本成功响应 + let result: Value = response.json().await + .unwrap_or_else(|_| serde_json::json!({})); + + let execute_response = ExecuteWorkflowResponse { + task_id: result.get("task_id").and_then(|v| v.as_str()).map(String::from), + status: result.get("status").and_then(|v| v.as_str()).map(String::from), + message: Some("Workflow execution started".to_string()), + result: Some(result), + error: None, + }; + info!("Workflow '{}' execution started", request.base_name); + Ok(execute_response) + } else { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + let execute_response = ExecuteWorkflowResponse { + task_id: None, + status: Some("failed".to_string()), + message: None, + result: None, + error: Some(format!("HTTP {} - {}", status, error_text)), + }; + warn!("Workflow '{}' execution failed: {}", request.base_name, error_text); + Ok(execute_response) + } + } + + /// 获取工作流规范 + /// + /// GET /api/spec/ + /// 获取指定工作流的规范信息 + pub async fn get_workflow_spec(&self, request: GetWorkflowSpecRequest) -> Result { + let mut url = format!("{}/api/spec/", self.config.base_url.trim_end_matches('/')); + + // 添加查询参数 + url.push_str(&format!("?base_name={}", urlencoding::encode(&request.base_name))); + if let Some(version) = &request.version { + url.push_str(&format!("&version={}", urlencoding::encode(version))); + } + + debug!("Getting workflow spec for '{}' from: {}", request.base_name, url); + + let response = self.execute_get_request(&url).await?; + let spec: Value = response.json().await + .map_err(|e| anyhow!("Failed to parse workflow spec response: {}", e))?; + + let spec_response = GetWorkflowSpecResponse { + spec: spec.clone(), + name: Some(request.base_name.clone()), + version: request.version.clone(), + description: spec.get("description").and_then(|v| v.as_str()).map(String::from), + }; + + info!("Retrieved workflow spec for '{}'", request.base_name); + Ok(spec_response) + } + + /// 获取服务器状态 + /// + /// GET /api/servers/status + /// 获取所有已配置的ComfyUI服务器的配置信息和实时状态 + pub async fn get_servers_status(&self) -> Result> { + let url = format!("{}/api/servers/status", self.config.base_url.trim_end_matches('/')); + debug!("Getting servers status from: {}", url); + + let response = self.execute_get_request(&url).await?; + let servers: Vec = response.json().await + .map_err(|e| anyhow!("Failed to parse servers status response: {}", e))?; + + info!("Retrieved status for {} servers", servers.len()); + Ok(servers) + } + + /// 获取服务器文件列表 + /// + /// GET /api/servers/{server_index}/files + /// 获取指定ComfyUI服务器的输入和输出文件夹中的文件列表 + pub async fn list_server_files(&self, server_index: i32) -> Result { + let url = format!("{}/api/servers/{}/files", self.config.base_url.trim_end_matches('/'), server_index); + debug!("Getting server files for server {} from: {}", server_index, url); + + let response = self.execute_get_request(&url).await?; + let files: ServerFiles = response.json().await + .map_err(|e| anyhow!("Failed to parse server files response: {}", e))?; + + info!("Retrieved file list for server {}: {} input files, {} output files", + server_index, files.input_files.len(), files.output_files.len()); + Ok(files) + } + + /// 获取 API 根信息 + /// + /// GET / + /// 提供一个API的快速使用指南 + pub async fn get_api_root(&self) -> Result { + let url = self.config.base_url.trim_end_matches('/').to_string(); + debug!("Getting API root info from: {}", url); + + let response = self.execute_get_request(&url).await?; + + // 尝试解析为结构化响应,如果失败则返回基本信息 + let result: Value = response.json().await + .unwrap_or_else(|_| serde_json::json!({"guide": "ComfyUI API"})); + + let root_response = ApiRootResponse { + guide: result.get("guide").and_then(|v| v.as_str()) + .unwrap_or("ComfyUI Workflow Service & Management API").to_string(), + version: result.get("version").and_then(|v| v.as_str()).map(String::from), + endpoints: result.get("endpoints").and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()), + }; + + info!("Retrieved API root information"); + Ok(root_response) + } + + // ========== 私有辅助方法 ========== + + /// 执行 GET 请求 + async fn execute_get_request(&self, url: &str) -> Result { + let response = self.client.get(url).send().await + .map_err(|e| anyhow!("GET request failed: {}", e))?; + Ok(response) + } + + /// 执行 POST 请求 + async fn execute_post_request(&self, url: &str, body: &T) -> Result { + let response = self.client.post(url).json(body).send().await + .map_err(|e| anyhow!("POST request failed: {}", e))?; + Ok(response) + } + + /// 执行 DELETE 请求 + async fn execute_delete_request(&self, url: &str) -> Result { + let response = self.client.delete(url).send().await + .map_err(|e| anyhow!("DELETE request failed: {}", e))?; + Ok(response) + } + + + + /// 验证配置 + pub fn validate_config(&self) -> Result<()> { + if self.config.base_url.is_empty() { + return Err(anyhow!("Base URL cannot be empty")); + } + + if !self.config.base_url.starts_with("http://") && !self.config.base_url.starts_with("https://") { + return Err(anyhow!("Base URL must start with http:// or https://")); + } + + if let Some(timeout) = self.config.timeout { + if timeout == 0 { + return Err(anyhow!("Timeout must be greater than 0")); + } + } + + if let Some(retry_attempts) = self.config.retry_attempts { + if retry_attempts > 10 { + warn!("Retry attempts is very high ({}), consider reducing it", retry_attempts); + } + } + + Ok(()) + } + + /// 获取服务配置 + pub fn get_config(&self) -> &ComfyuiConfig { + &self.config + } + + /// 更新服务配置 + pub fn update_config(&mut self, config: ComfyuiConfig) -> Result<()> { + // 验证新配置 + let temp_service = Self::new(config.clone())?; + temp_service.validate_config()?; + + // 更新配置和客户端 + self.config = config; + let timeout = Duration::from_secs(self.config.timeout.unwrap_or(30)); + self.client = Client::builder() + .timeout(timeout) + .build() + .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?; + + info!("ComfyUI service configuration updated"); + Ok(()) + } + + /// 测试连接 + pub async fn test_connection(&self) -> Result { + match self.get_api_root().await { + Ok(_) => { + info!("ComfyUI service connection test successful"); + Ok(true) + } + Err(e) => { + error!("ComfyUI service connection test failed: {}", e); + Ok(false) + } + } + } +} diff --git a/apps/desktop/src-tauri/src/infrastructure/mod.rs b/apps/desktop/src-tauri/src/infrastructure/mod.rs index a5a90e3..4cfc617 100644 --- a/apps/desktop/src-tauri/src/infrastructure/mod.rs +++ b/apps/desktop/src-tauri/src/infrastructure/mod.rs @@ -19,3 +19,4 @@ pub mod image_editing_service; pub mod tolerant_json_parser; pub mod markdown_parser; pub mod bowong_text_video_agent_service; +pub mod comfyui_service; diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index f19cd19..4b00567 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -560,6 +560,18 @@ pub fn run() { commands::bowong_text_video_agent_commands::bowong_cancel_tasks, commands::bowong_text_video_agent_commands::bowong_batch_query_task_status, commands::bowong_text_video_agent_commands::bowong_wait_for_task_completion, + // ComfyUI API 命令 + commands::comfyui_commands::comfyui_get_all_workflows, + commands::comfyui_commands::comfyui_publish_workflow, + commands::comfyui_commands::comfyui_delete_workflow, + commands::comfyui_commands::comfyui_execute_workflow, + commands::comfyui_commands::comfyui_get_workflow_spec, + commands::comfyui_commands::comfyui_get_servers_status, + commands::comfyui_commands::comfyui_list_server_files, + commands::comfyui_commands::comfyui_get_api_root, + commands::comfyui_commands::comfyui_test_connection, + commands::comfyui_commands::comfyui_get_config, + commands::comfyui_commands::comfyui_update_config, // Hedra 口型合成命令 commands::bowong_text_video_agent_commands::hedra_upload_file, commands::bowong_text_video_agent_commands::hedra_submit_task, @@ -627,6 +639,20 @@ pub fn run() { // 不返回错误,让应用继续启动,只是记录错误 } + // 初始化 ComfyUI 服务 + let comfyui_config = data::models::comfyui::ComfyuiConfig { + base_url: "https://bowongai-dev--waas-demo-fastapi-webapp.modal.run".to_string(), + timeout: Some(30), + retry_attempts: Some(3), + enable_cache: Some(true), + max_concurrency: Some(8), + }; + + if let Err(e) = state.initialize_comfyui_service(comfyui_config) { + eprintln!("Failed to initialize ComfyUI service: {}", e); + // 不返回错误,让应用继续启动,只是记录错误 + } + Ok(()) }) .run(tauri::generate_context!()) diff --git a/apps/desktop/src-tauri/src/presentation/commands/comfyui_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/comfyui_commands.rs new file mode 100644 index 0000000..f4c207b --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/comfyui_commands.rs @@ -0,0 +1,334 @@ +use crate::app_state::AppState; +use crate::data::models::comfyui::*; +use anyhow::Result; +use tauri::State; +use tracing::{error, info}; + +/// 获取所有工作流 +/// +/// 前端调用示例: +/// ```typescript +/// const workflows = await invoke('comfyui_get_all_workflows'); +/// ``` +#[tauri::command] +pub async fn comfyui_get_all_workflows( + state: State<'_, AppState>, +) -> Result, String> { + info!("Command: comfyui_get_all_workflows"); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.get_all_workflows().await { + Ok(workflows) => { + info!("Successfully retrieved {} workflows", workflows.len()); + Ok(workflows) + } + Err(e) => { + error!("Failed to get workflows: {}", e); + Err(format!("Failed to get workflows: {}", e)) + } + } +} + +/// 发布工作流 +/// +/// 前端调用示例: +/// ```typescript +/// const request = { +/// name: "my_workflow", +/// workflow_data: { /* workflow config */ }, +/// description: "My test workflow", +/// version: "1.0" +/// }; +/// const response = await invoke('comfyui_publish_workflow', { request }); +/// ``` +#[tauri::command] +pub async fn comfyui_publish_workflow( + request: PublishWorkflowRequest, + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_publish_workflow - {}", request.name); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.publish_workflow(request).await { + Ok(response) => { + info!("Successfully published workflow"); + Ok(response) + } + Err(e) => { + error!("Failed to publish workflow: {}", e); + Err(format!("Failed to publish workflow: {}", e)) + } + } +} + +/// 删除工作流 +/// +/// 前端调用示例: +/// ```typescript +/// const response = await invoke('comfyui_delete_workflow', { +/// workflowName: "my_workflow [20250101120000]" +/// }); +/// ``` +#[tauri::command] +pub async fn comfyui_delete_workflow( + workflow_name: String, + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_delete_workflow - {}", workflow_name); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.delete_workflow(&workflow_name).await { + Ok(response) => { + info!("Successfully deleted workflow: {}", workflow_name); + Ok(response) + } + Err(e) => { + error!("Failed to delete workflow {}: {}", workflow_name, e); + Err(format!("Failed to delete workflow: {}", e)) + } + } +} + +/// 执行工作流 +/// +/// 前端调用示例: +/// ```typescript +/// const request = { +/// base_name: "my_workflow", +/// version: "1.0", // 可选 +/// request_data: { /* execution parameters */ } +/// }; +/// const response = await invoke('comfyui_execute_workflow', { request }); +/// ``` +#[tauri::command] +pub async fn comfyui_execute_workflow( + request: ExecuteWorkflowRequest, + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_execute_workflow - {}", request.base_name); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.execute_workflow(request).await { + Ok(response) => { + info!("Successfully executed workflow"); + Ok(response) + } + Err(e) => { + error!("Failed to execute workflow: {}", e); + Err(format!("Failed to execute workflow: {}", e)) + } + } +} + +/// 获取工作流规范 +/// +/// 前端调用示例: +/// ```typescript +/// const request = { +/// base_name: "my_workflow", +/// version: "1.0" // 可选 +/// }; +/// const response = await invoke('comfyui_get_workflow_spec', { request }); +/// ``` +#[tauri::command] +pub async fn comfyui_get_workflow_spec( + request: GetWorkflowSpecRequest, + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_get_workflow_spec - {}", request.base_name); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.get_workflow_spec(request).await { + Ok(response) => { + info!("Successfully retrieved workflow spec"); + Ok(response) + } + Err(e) => { + error!("Failed to get workflow spec: {}", e); + Err(format!("Failed to get workflow spec: {}", e)) + } + } +} + +/// 获取服务器状态 +/// +/// 前端调用示例: +/// ```typescript +/// const servers = await invoke('comfyui_get_servers_status'); +/// ``` +#[tauri::command] +pub async fn comfyui_get_servers_status( + state: State<'_, AppState>, +) -> Result, String> { + info!("Command: comfyui_get_servers_status"); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.get_servers_status().await { + Ok(servers) => { + info!("Successfully retrieved status for {} servers", servers.len()); + Ok(servers) + } + Err(e) => { + error!("Failed to get servers status: {}", e); + Err(format!("Failed to get servers status: {}", e)) + } + } +} + +/// 获取服务器文件列表 +/// +/// 前端调用示例: +/// ```typescript +/// const files = await invoke('comfyui_list_server_files', { serverIndex: 0 }); +/// ``` +#[tauri::command] +pub async fn comfyui_list_server_files( + server_index: i32, + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_list_server_files - server {}", server_index); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.list_server_files(server_index).await { + Ok(files) => { + info!("Successfully retrieved files for server {}", server_index); + Ok(files) + } + Err(e) => { + error!("Failed to get server files for server {}: {}", server_index, e); + Err(format!("Failed to get server files: {}", e)) + } + } +} + +/// 获取 API 根信息 +/// +/// 前端调用示例: +/// ```typescript +/// const apiInfo = await invoke('comfyui_get_api_root'); +/// ``` +#[tauri::command] +pub async fn comfyui_get_api_root( + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_get_api_root"); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.get_api_root().await { + Ok(response) => { + info!("Successfully retrieved API root information"); + Ok(response) + } + Err(e) => { + error!("Failed to get API root: {}", e); + Err(format!("Failed to get API root: {}", e)) + } + } +} + +/// 测试 ComfyUI 服务连接 +/// +/// 前端调用示例: +/// ```typescript +/// const isConnected = await invoke('comfyui_test_connection'); +/// ``` +#[tauri::command] +pub async fn comfyui_test_connection( + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_test_connection"); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + match comfyui_service.test_connection().await { + Ok(is_connected) => { + info!("Connection test result: {}", is_connected); + Ok(is_connected) + } + Err(e) => { + error!("Failed to test connection: {}", e); + Err(format!("Failed to test connection: {}", e)) + } + } +} + +/// 获取 ComfyUI 服务配置 +/// +/// 前端调用示例: +/// ```typescript +/// const config = await invoke('comfyui_get_config'); +/// ``` +#[tauri::command] +pub async fn comfyui_get_config( + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_get_config"); + + let app_state = state.inner(); + let comfyui_service = app_state.get_comfyui_service() + .ok_or_else(|| "ComfyUI service not initialized".to_string())?; + + let config = comfyui_service.get_config().clone(); + info!("Successfully retrieved ComfyUI configuration"); + Ok(config) +} + +/// 更新 ComfyUI 服务配置 +/// +/// 前端调用示例: +/// ```typescript +/// const newConfig = { +/// base_url: "https://new-api-url.com", +/// timeout: 60, +/// retry_attempts: 5, +/// enable_cache: true, +/// max_concurrency: 10 +/// }; +/// const success = await invoke('comfyui_update_config', { config: newConfig }); +/// ``` +#[tauri::command] +pub async fn comfyui_update_config( + config: ComfyuiConfig, + state: State<'_, AppState>, +) -> Result { + info!("Command: comfyui_update_config"); + + let app_state = state.inner(); + + match app_state.update_comfyui_config(config).await { + Ok(_) => { + info!("Successfully updated ComfyUI configuration"); + Ok(true) + } + Err(e) => { + error!("Failed to update ComfyUI configuration: {}", e); + Err(format!("Failed to update configuration: {}", e)) + } + } +} diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index 87b65b4..8d4cfef 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/mod.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/mod.rs @@ -43,3 +43,4 @@ pub mod error_handling_commands; pub mod volcano_video_commands; pub mod bowong_text_video_agent_commands; pub mod hedra_lipsync_commands; +pub mod comfyui_commands; diff --git a/comfyui.json b/comfyui.json new file mode 100644 index 0000000..2bb3df3 --- /dev/null +++ b/comfyui.json @@ -0,0 +1,465 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "ComfyUI Workflow Service & Management API", + "version": "0.1.0" + }, + "paths": { + "/api/workflow": { + "get": { + "summary": "Get All Workflows Endpoint", + "operationId": "get_all_workflows_endpoint_api_workflow_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Response Get All Workflows Endpoint Api Workflow Get" + } + } + } + } + } + }, + "post": { + "summary": "Publish Workflow Endpoint", + "operationId": "publish_workflow_endpoint_api_workflow_post", + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/workflow/{workflow_name}": { + "delete": { + "summary": "Delete Workflow Endpoint", + "operationId": "delete_workflow_endpoint_api_workflow__workflow_name__delete", + "parameters": [ + { + "name": "workflow_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The full, unique name of the workflow to delete, e.g., 'my_workflow [20250101120000]'", + "title": "Workflow Name" + }, + "description": "The full, unique name of the workflow to delete, e.g., 'my_workflow [20250101120000]'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/run/": { + "post": { + "summary": "Execute Workflow Endpoint", + "operationId": "execute_workflow_endpoint_api_run__post", + "parameters": [ + { + "name": "base_name", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Base Name" + } + }, + { + "name": "version", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Request Data Raw" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/spec/": { + "get": { + "summary": "Get Workflow Spec Endpoint", + "operationId": "get_workflow_spec_endpoint_api_spec__get", + "parameters": [ + { + "name": "base_name", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Base Name" + } + }, + { + "name": "version", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/servers/status": { + "get": { + "tags": [ + "Server Monitoring" + ], + "summary": "Get Servers Status Endpoint", + "description": "获取所有已配置的ComfyUI服务器的配置信息和实时状态。", + "operationId": "get_servers_status_endpoint_api_servers_status_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ServerStatus" + }, + "type": "array", + "title": "Response Get Servers Status Endpoint Api Servers Status Get" + } + } + } + } + } + } + }, + "/api/servers/{server_index}/files": { + "get": { + "tags": [ + "Server Monitoring" + ], + "summary": "List Server Files Endpoint", + "description": "获取指定ComfyUI服务器的输入和输出文件夹中的文件列表。", + "operationId": "list_server_files_endpoint_api_servers__server_index__files_get", + "parameters": [ + { + "name": "server_index", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 0, + "description": "服务器在配置列表中的索引", + "title": "Server Index" + }, + "description": "服务器在配置列表中的索引" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerFiles" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/": { + "get": { + "summary": "Read Root", + "description": "提供一个API的快速使用指南。", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + } + }, + "components": { + "schemas": { + "FileDetails": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "size_kb": { + "type": "number", + "title": "Size Kb" + }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + } + }, + "type": "object", + "required": [ + "name", + "size_kb", + "modified_at" + ], + "title": "FileDetails" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "ServerFiles": { + "properties": { + "server_index": { + "type": "integer", + "title": "Server Index" + }, + "http_url": { + "type": "string", + "title": "Http Url" + }, + "input_files": { + "items": { + "$ref": "#/components/schemas/FileDetails" + }, + "type": "array", + "title": "Input Files" + }, + "output_files": { + "items": { + "$ref": "#/components/schemas/FileDetails" + }, + "type": "array", + "title": "Output Files" + } + }, + "type": "object", + "required": [ + "server_index", + "http_url", + "input_files", + "output_files" + ], + "title": "ServerFiles" + }, + "ServerQueueDetails": { + "properties": { + "running_count": { + "type": "integer", + "title": "Running Count" + }, + "pending_count": { + "type": "integer", + "title": "Pending Count" + } + }, + "type": "object", + "required": [ + "running_count", + "pending_count" + ], + "title": "ServerQueueDetails" + }, + "ServerStatus": { + "properties": { + "server_index": { + "type": "integer", + "title": "Server Index" + }, + "http_url": { + "type": "string", + "title": "Http Url" + }, + "ws_url": { + "type": "string", + "title": "Ws Url" + }, + "input_dir": { + "type": "string", + "title": "Input Dir" + }, + "output_dir": { + "type": "string", + "title": "Output Dir" + }, + "is_reachable": { + "type": "boolean", + "title": "Is Reachable" + }, + "is_free": { + "type": "boolean", + "title": "Is Free" + }, + "queue_details": { + "$ref": "#/components/schemas/ServerQueueDetails" + } + }, + "type": "object", + "required": [ + "server_index", + "http_url", + "ws_url", + "input_dir", + "output_dir", + "is_reachable", + "is_free", + "queue_details" + ], + "title": "ServerStatus" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + } +} \ No newline at end of file