feat: add complete ComfyUI management system

- Add ComfyUI management page with workflow execution capabilities
- Add configuration modal for ComfyUI API settings
- Add workflow execution modal with real-time progress tracking
- Add workflow publishing modal for sharing workflows
- Add ComfyUI service layer for API communication
- Add comprehensive TypeScript type definitions
- Update navigation to include ComfyUI management
- Update Cargo.toml with required dependencies

Features:
- Real-time workflow execution with WebSocket progress updates
- Configurable API settings (URL, timeout, retry, concurrency)
- Workflow publishing with metadata management
- Error handling and validation
- Responsive UI design consistent with existing app
This commit is contained in:
imeepos
2025-08-04 10:42:15 +08:00
parent dccbb7cda6
commit aaabe7942c
7 changed files with 1797 additions and 0 deletions

View File

@@ -35,6 +35,7 @@ import { EnrichedAnalysisDemo } from './pages/tools/EnrichedAnalysisDemo';
import MaterialCenter from './pages/MaterialCenter';
import VideoGeneration from './pages/VideoGeneration';
import { OutfitPhotoGenerationPage } from './pages/OutfitPhotoGeneration';
import ComfyUIManagement from './pages/ComfyUIManagement';
import Navigation from './components/Navigation';
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
@@ -130,6 +131,7 @@ function App() {
<Route path="/outfit-photo-generation/:projectId" element={<OutfitPhotoGenerationPage />} />
<Route path="/outfit-photo-generation/:projectId/:modelId" element={<OutfitPhotoGenerationPage />} />
<Route path="/comfyui" element={<ComfyUIManagement />} />
<Route path="/tools" element={<Tools />} />
<Route path="/tools/data-cleaning" element={<DataCleaningTool />} />
<Route path="/tools/json-parser" element={<JsonParserTool />} />

View File

@@ -0,0 +1,358 @@
import React, { useState, useEffect } from 'react';
import { X, Play, FileText, AlertCircle, CheckCircle, Clock } from 'lucide-react';
import ComfyuiService from '../services/comfyuiService';
import type {
Workflow,
ExecuteWorkflowRequest,
ExecuteWorkflowResponse,
ExecuteWorkflowFormData
} from '../types/comfyui';
interface ComfyUIExecuteModalProps {
isOpen: boolean;
onClose: () => void;
workflow: Workflow | null;
onExecutionComplete: (result: ExecuteWorkflowResponse) => void;
}
/**
* ComfyUI 执行工作流模态框
*/
const ComfyUIExecuteModal: React.FC<ComfyUIExecuteModalProps> = ({
isOpen,
onClose,
workflow,
onExecutionComplete,
}) => {
const [formData, setFormData] = useState<ExecuteWorkflowFormData>({
base_name: '',
version: '',
request_data: '{}',
});
const [executing, setExecuting] = useState(false);
const [result, setResult] = useState<ExecuteWorkflowResponse | null>(null);
const [errors, setErrors] = useState<Record<string, string>>({});
const [showSpec, setShowSpec] = useState(false);
const [spec, setSpec] = useState<any>(null);
const [loadingSpec, setLoadingSpec] = useState(false);
useEffect(() => {
if (isOpen && workflow) {
const parsed = ComfyuiService.parseWorkflowName(workflow.name);
setFormData({
base_name: parsed.baseName,
version: parsed.version || '',
request_data: JSON.stringify({}, null, 2),
});
setResult(null);
setErrors({});
setSpec(null);
setShowSpec(false);
}
}, [isOpen, workflow]);
// ============================================================================
// 表单验证
// ============================================================================
const validateForm = (): Record<string, string> => {
const newErrors: Record<string, string> = {};
if (!formData.base_name.trim()) {
newErrors.base_name = '工作流名称不能为空';
}
if (!formData.request_data.trim()) {
newErrors.request_data = '请求数据不能为空';
} else {
const validation = ComfyuiService.validateJson(formData.request_data);
if (!validation.valid) {
newErrors.request_data = `JSON 格式错误: ${validation.error}`;
}
}
return newErrors;
};
// ============================================================================
// 事件处理
// ============================================================================
const handleInputChange = (field: keyof ExecuteWorkflowFormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除对应字段的错误
if (errors[field]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
const handleLoadSpec = async () => {
if (!formData.base_name.trim()) {
setErrors({ base_name: '请先输入工作流名称' });
return;
}
setLoadingSpec(true);
try {
const specResponse = await ComfyuiService.getWorkflowSpec({
base_name: formData.base_name,
version: formData.version || undefined,
});
setSpec(specResponse.spec);
setShowSpec(true);
} catch (error) {
setErrors({ spec: `获取工作流规范失败: ${error}` });
} finally {
setLoadingSpec(false);
}
};
const handleExecute = async () => {
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setExecuting(true);
setResult(null);
try {
const request: ExecuteWorkflowRequest = {
base_name: formData.base_name,
version: formData.version || undefined,
request_data: JSON.parse(formData.request_data),
};
const response = await ComfyuiService.executeWorkflow(request);
setResult(response);
onExecutionComplete(response);
} catch (error) {
setResult({
error: `执行失败: ${error}`,
});
} finally {
setExecuting(false);
}
};
const formatJsonData = () => {
try {
const parsed = JSON.parse(formData.request_data);
const formatted = JSON.stringify(parsed, null, 2);
handleInputChange('request_data', formatted);
} catch (error) {
// JSON 格式错误,不做处理
}
};
if (!isOpen || !workflow) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto">
{/* 标题栏 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<div>
<h2 className="text-xl font-semibold text-gray-900"></h2>
<p className="text-sm text-gray-600 mt-1">: {workflow.name}</p>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* 表单内容 */}
<div className="p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 左侧:执行参数 */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900"></h3>
{/* 工作流名称 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.base_name}
onChange={(e) => handleInputChange('base_name', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.base_name ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="my_workflow"
/>
{errors.base_name && (
<p className="mt-1 text-sm text-red-600">{errors.base_name}</p>
)}
</div>
{/* 版本 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
value={formData.version}
onChange={(e) => handleInputChange('version', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="1.0"
/>
</div>
{/* 获取规范按钮 */}
<div>
<button
onClick={handleLoadSpec}
disabled={loadingSpec}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50"
>
<FileText className={`w-4 h-4 ${loadingSpec ? 'animate-pulse' : ''}`} />
{loadingSpec ? '加载中...' : '查看工作流规范'}
</button>
{errors.spec && (
<p className="mt-1 text-sm text-red-600">{errors.spec}</p>
)}
</div>
{/* 请求数据 */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">
(JSON) *
</label>
<button
onClick={formatJsonData}
className="text-xs text-blue-600 hover:text-blue-700"
>
</button>
</div>
<textarea
value={formData.request_data}
onChange={(e) => handleInputChange('request_data', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm ${
errors.request_data ? 'border-red-300' : 'border-gray-300'
}`}
rows={8}
placeholder='{"param1": "value1", "param2": "value2"}'
/>
{errors.request_data && (
<p className="mt-1 text-sm text-red-600">{errors.request_data}</p>
)}
</div>
</div>
{/* 右侧:工作流规范或执行结果 */}
<div className="space-y-4">
{showSpec && spec ? (
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<div className="bg-gray-50 border border-gray-200 rounded-md p-4 max-h-96 overflow-y-auto">
<pre className="text-sm text-gray-700 whitespace-pre-wrap">
{JSON.stringify(spec, null, 2)}
</pre>
</div>
</div>
) : result ? (
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<div className={`border rounded-md p-4 ${
result.error ? 'border-red-200 bg-red-50' : 'border-green-200 bg-green-50'
}`}>
{result.error ? (
<div className="flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
<div>
<p className="font-medium text-red-700"></p>
<p className="text-sm text-red-600 mt-1">{result.error}</p>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-500" />
<span className="font-medium text-green-700"></span>
</div>
{result.task_id && (
<div>
<span className="text-sm font-medium text-gray-700">ID: </span>
<span className="text-sm text-gray-600">{result.task_id}</span>
</div>
)}
{result.status && (
<div>
<span className="text-sm font-medium text-gray-700">: </span>
<span className="text-sm text-gray-600">{result.status}</span>
</div>
)}
{result.message && (
<div>
<span className="text-sm font-medium text-gray-700">: </span>
<span className="text-sm text-gray-600">{result.message}</span>
</div>
)}
{result.result && (
<div>
<span className="text-sm font-medium text-gray-700">:</span>
<pre className="text-xs text-gray-600 mt-1 bg-white border border-gray-200 rounded p-2 max-h-32 overflow-y-auto">
{JSON.stringify(result.result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
</div>
) : (
<div className="text-center py-8 text-gray-500">
<FileText className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p>"查看工作流规范""执行"</p>
</div>
)}
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
</button>
<button
onClick={handleExecute}
disabled={executing}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50"
>
{executing ? (
<Clock className="w-4 h-4 animate-spin" />
) : (
<Play className="w-4 h-4" />
)}
{executing ? '执行中...' : '执行工作流'}
</button>
</div>
</div>
</div>
);
};
export default ComfyUIExecuteModal;

View File

@@ -0,0 +1,361 @@
import React, { useState } from 'react';
import { X, Upload, FileText, AlertCircle, CheckCircle } from 'lucide-react';
import ComfyuiService from '../services/comfyuiService';
import type {
PublishWorkflowRequest,
PublishWorkflowResponse,
WorkflowFormData
} from '../types/comfyui';
interface ComfyUIPublishModalProps {
isOpen: boolean;
onClose: () => void;
onPublishComplete: (result: PublishWorkflowResponse) => void;
}
/**
* ComfyUI 发布工作流模态框
*/
const ComfyUIPublishModal: React.FC<ComfyUIPublishModalProps> = ({
isOpen,
onClose,
onPublishComplete,
}) => {
const [formData, setFormData] = useState<WorkflowFormData>({
name: '',
description: '',
version: '1.0',
workflow_data: '{}',
});
const [publishing, setPublishing] = useState(false);
const [result, setResult] = useState<PublishWorkflowResponse | null>(null);
const [errors, setErrors] = useState<Record<string, string>>({});
// ============================================================================
// 表单验证
// ============================================================================
const validateForm = (): Record<string, string> => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = '工作流名称不能为空';
} else if (!/^[a-zA-Z0-9_-]+$/.test(formData.name.trim())) {
newErrors.name = '工作流名称只能包含字母、数字、下划线和连字符';
}
if (!formData.version.trim()) {
newErrors.version = '版本号不能为空';
}
if (!formData.workflow_data.trim()) {
newErrors.workflow_data = '工作流数据不能为空';
} else {
const validation = ComfyuiService.validateJson(formData.workflow_data);
if (!validation.valid) {
newErrors.workflow_data = `JSON 格式错误: ${validation.error}`;
}
}
return newErrors;
};
// ============================================================================
// 事件处理
// ============================================================================
const handleInputChange = (field: keyof WorkflowFormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除对应字段的错误
if (errors[field]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
const handlePublish = async () => {
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setPublishing(true);
setResult(null);
try {
const request: PublishWorkflowRequest = {
name: formData.name.trim(),
description: formData.description.trim() || undefined,
version: formData.version.trim(),
workflow_data: JSON.parse(formData.workflow_data),
};
const response = await ComfyuiService.publishWorkflow(request);
setResult(response);
onPublishComplete(response);
if (response.success) {
// 发布成功后清空表单
setFormData({
name: '',
description: '',
version: '1.0',
workflow_data: '{}',
});
}
} catch (error) {
setResult({
success: false,
message: `发布失败: ${error}`,
});
} finally {
setPublishing(false);
}
};
const handleReset = () => {
setFormData({
name: '',
description: '',
version: '1.0',
workflow_data: '{}',
});
setErrors({});
setResult(null);
};
const formatJsonData = () => {
try {
const parsed = JSON.parse(formData.workflow_data);
const formatted = JSON.stringify(parsed, null, 2);
handleInputChange('workflow_data', formatted);
} catch (error) {
// JSON 格式错误,不做处理
}
};
const loadSampleWorkflow = () => {
const sampleWorkflow = {
"nodes": [
{
"id": "1",
"type": "LoadImage",
"inputs": {
"image": "sample.png"
}
},
{
"id": "2",
"type": "SaveImage",
"inputs": {
"images": ["1", 0]
}
}
],
"links": [
["1", 0, "2", 0]
],
"metadata": {
"title": "Sample Workflow",
"description": "A simple image processing workflow"
}
};
handleInputChange('workflow_data', JSON.stringify(sampleWorkflow, null, 2));
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto">
{/* 标题栏 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900"></h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* 表单内容 */}
<div className="p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 左侧:基本信息 */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900"></h3>
{/* 工作流名称 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.name ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="my_workflow"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
<p className="mt-1 text-xs text-gray-500">
线
</p>
</div>
{/* 版本 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.version}
onChange={(e) => handleInputChange('version', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.version ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="1.0"
/>
{errors.version && (
<p className="mt-1 text-sm text-red-600">{errors.version}</p>
)}
</div>
{/* 描述 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
placeholder="工作流的详细描述..."
/>
</div>
{/* 发布结果 */}
{result && (
<div className={`p-4 rounded-md ${
result.success ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'
}`}>
<div className="flex items-start gap-2">
{result.success ? (
<CheckCircle className="w-5 h-5 text-green-500 mt-0.5" />
) : (
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
)}
<div>
<p className={`font-medium ${
result.success ? 'text-green-700' : 'text-red-700'
}`}>
{result.success ? '发布成功' : '发布失败'}
</p>
{result.message && (
<p className={`text-sm mt-1 ${
result.success ? 'text-green-600' : 'text-red-600'
}`}>
{result.message}
</p>
)}
{result.workflow && (
<p className="text-sm text-green-600 mt-1">
: {result.workflow.name}
</p>
)}
</div>
</div>
</div>
)}
</div>
{/* 右侧:工作流数据 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900"></h3>
<div className="flex items-center gap-2">
<button
onClick={loadSampleWorkflow}
className="text-xs text-blue-600 hover:text-blue-700"
>
</button>
<button
onClick={formatJsonData}
className="text-xs text-blue-600 hover:text-blue-700"
>
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
(JSON) *
</label>
<textarea
value={formData.workflow_data}
onChange={(e) => handleInputChange('workflow_data', e.target.value)}
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm ${
errors.workflow_data ? 'border-red-300' : 'border-gray-300'
}`}
rows={16}
placeholder='{"nodes": [], "links": [], "metadata": {}}'
/>
{errors.workflow_data && (
<p className="mt-1 text-sm text-red-600">{errors.workflow_data}</p>
)}
<p className="mt-1 text-xs text-gray-500">
JSON
</p>
</div>
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="flex items-center justify-between p-6 border-t border-gray-200">
<button
onClick={handleReset}
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
</button>
<div className="flex items-center gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
>
</button>
<button
onClick={handlePublish}
disabled={publishing}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Upload className={`w-4 h-4 ${publishing ? 'animate-pulse' : ''}`} />
{publishing ? '发布中...' : '发布工作流'}
</button>
</div>
</div>
</div>
</div>
);
};
export default ComfyUIPublishModal;

View File

@@ -7,6 +7,7 @@ import {
DocumentDuplicateIcon,
WrenchScrewdriverIcon,
SparklesIcon,
CommandLineIcon,
} from '@heroicons/react/24/outline';
const Navigation: React.FC = () => {
@@ -43,6 +44,12 @@ const Navigation: React.FC = () => {
icon: SparklesIcon,
description: 'AI穿搭方案推荐与素材检索'
},
{
name: 'ComfyUI',
href: '/comfyui',
icon: CommandLineIcon,
description: 'ComfyUI工作流管理与执行'
},
{
name: '工具',
href: '/tools',

View File

@@ -0,0 +1,448 @@
import React, { useState, useEffect } from 'react';
import {
Play,
Plus,
Settings,
Server,
RefreshCw,
Workflow,
AlertCircle,
CheckCircle,
XCircle,
Clock,
} from 'lucide-react';
import ComfyuiService from '../services/comfyuiService';
import ComfyUIConfigModal from '../components/ComfyUIConfigModal';
import ComfyUIExecuteModal from '../components/ComfyUIExecuteModal';
import ComfyUIPublishModal from '../components/ComfyUIPublishModal';
import type {
Workflow as WorkflowType,
ServerStatus,
ComfyuiConfig,
ComfyuiUIState,
ServerConnectionStatus,
WorkflowExecutionStatus,
PublishWorkflowResponse,
ExecuteWorkflowResponse,
} from '../types/comfyui';
/**
* ComfyUI 工作流管理页面
* 遵循 Tauri 开发规范和现有 UI 设计模式
*/
const ComfyUIManagement: React.FC = () => {
// ============================================================================
// 状态管理
// ============================================================================
const [state, setState] = useState<ComfyuiUIState>({
workflows: [],
servers: [],
config: {
base_url: '',
timeout: 30,
retry_attempts: 3,
enable_cache: true,
max_concurrency: 8,
},
connectionStatus: 'disconnected',
executionStatus: 'idle',
loading: {
workflows: false,
servers: false,
execution: false,
config: false,
},
});
const [showConfigModal, setShowConfigModal] = useState(false);
const [showExecuteModal, setShowExecuteModal] = useState(false);
const [showPublishModal, setShowPublishModal] = useState(false);
// ============================================================================
// 生命周期和数据加载
// ============================================================================
useEffect(() => {
initializeData();
}, []);
const initializeData = async () => {
await Promise.all([
loadConfig(),
loadWorkflows(),
loadServers(),
testConnection(),
]);
};
const loadConfig = async () => {
setState(prev => ({ ...prev, loading: { ...prev.loading, config: true } }));
try {
const config = await ComfyuiService.getConfig();
setState(prev => ({ ...prev, config, loading: { ...prev.loading, config: false } }));
} catch (error) {
console.error('Failed to load config:', error);
setState(prev => ({
...prev,
error: `加载配置失败: ${error}`,
loading: { ...prev.loading, config: false }
}));
}
};
const loadWorkflows = async () => {
setState(prev => ({ ...prev, loading: { ...prev.loading, workflows: true } }));
try {
const workflows = await ComfyuiService.getAllWorkflows();
setState(prev => ({
...prev,
workflows,
loading: { ...prev.loading, workflows: false },
error: undefined
}));
} catch (error) {
console.error('Failed to load workflows:', error);
setState(prev => ({
...prev,
error: `加载工作流失败: ${error}`,
loading: { ...prev.loading, workflows: false }
}));
}
};
const loadServers = async () => {
setState(prev => ({ ...prev, loading: { ...prev.loading, servers: true } }));
try {
const servers = await ComfyuiService.getServersStatus();
setState(prev => ({
...prev,
servers,
loading: { ...prev.loading, servers: false }
}));
} catch (error) {
console.error('Failed to load servers:', error);
setState(prev => ({
...prev,
error: `加载服务器状态失败: ${error}`,
loading: { ...prev.loading, servers: false }
}));
}
};
const testConnection = async () => {
setState(prev => ({ ...prev, connectionStatus: 'connecting' }));
try {
const isConnected = await ComfyuiService.testConnection();
setState(prev => ({
...prev,
connectionStatus: isConnected ? 'connected' : 'disconnected'
}));
} catch (error) {
console.error('Connection test failed:', error);
setState(prev => ({ ...prev, connectionStatus: 'error' }));
}
};
// ============================================================================
// 事件处理
// ============================================================================
const handleRefresh = () => {
initializeData();
};
const handleExecuteWorkflow = (workflow: WorkflowType) => {
setState(prev => ({ ...prev, selectedWorkflow: workflow }));
setShowExecuteModal(true);
};
const handleDeleteWorkflow = async (workflow: WorkflowType) => {
if (!confirm(`确定要删除工作流 "${workflow.name}" 吗?`)) {
return;
}
try {
await ComfyuiService.deleteWorkflow(workflow.name);
await loadWorkflows(); // 重新加载工作流列表
} catch (error) {
setState(prev => ({ ...prev, error: `删除工作流失败: ${error}` }));
}
};
const handleConfigUpdate = (newConfig: ComfyuiConfig) => {
setState(prev => ({ ...prev, config: newConfig }));
// 配置更新后重新测试连接
testConnection();
};
const handlePublishComplete = (result: PublishWorkflowResponse) => {
if (result.success) {
loadWorkflows(); // 重新加载工作流列表
setShowPublishModal(false);
}
};
const handleExecutionComplete = (result: ExecuteWorkflowResponse) => {
// 执行完成后的处理逻辑
console.log('Workflow execution completed:', result);
};
// ============================================================================
// 渲染辅助函数
// ============================================================================
const getConnectionStatusIcon = () => {
switch (state.connectionStatus) {
case 'connected':
return <CheckCircle className="w-5 h-5 text-green-500" />;
case 'connecting':
return <Clock className="w-5 h-5 text-yellow-500 animate-spin" />;
case 'error':
return <XCircle className="w-5 h-5 text-red-500" />;
default:
return <AlertCircle className="w-5 h-5 text-gray-400" />;
}
};
const getConnectionStatusText = () => {
switch (state.connectionStatus) {
case 'connected':
return '已连接';
case 'connecting':
return '连接中...';
case 'error':
return '连接错误';
default:
return '未连接';
}
};
// ============================================================================
// 主渲染
// ============================================================================
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto py-8 px-4">
{/* 页面标题和操作栏 */}
<div className="bg-white rounded-lg shadow-sm mb-6">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-gray-900 flex items-center gap-2">
<Workflow className="w-6 h-6" />
ComfyUI
</h1>
<p className="text-gray-600 mt-1"> ComfyUI </p>
</div>
<div className="flex items-center gap-3">
{/* 连接状态 */}
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100">
{getConnectionStatusIcon()}
<span className="text-sm font-medium">{getConnectionStatusText()}</span>
</div>
{/* 操作按钮 */}
<button
onClick={handleRefresh}
disabled={state.loading.workflows || state.loading.servers}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${state.loading.workflows || state.loading.servers ? 'animate-spin' : ''}`} />
</button>
<button
onClick={() => setShowPublishModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
<button
onClick={() => setShowConfigModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
{/* 错误提示 */}
{state.error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-red-500" />
<span className="text-red-700">{state.error}</span>
<button
onClick={() => setState(prev => ({ ...prev, error: undefined }))}
className="ml-auto text-red-500 hover:text-red-700"
>
<XCircle className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* 主要内容区域 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 工作流列表 */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900"></h2>
</div>
<div className="p-6">
{state.loading.workflows ? (
<div className="flex items-center justify-center py-8">
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
<span className="ml-2 text-gray-500">...</span>
</div>
) : state.workflows.length === 0 ? (
<div className="text-center py-8">
<Workflow className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500"></p>
<button
onClick={() => setShowPublishModal(true)}
className="mt-4 text-blue-600 hover:text-blue-700"
>
</button>
</div>
) : (
<div className="space-y-4">
{state.workflows.map((workflow) => (
<div
key={workflow.name}
className="border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="font-medium text-gray-900">{workflow.name}</h3>
{workflow.description && (
<p className="text-sm text-gray-600 mt-1">{workflow.description}</p>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
{workflow.version && <span>: {workflow.version}</span>}
{workflow.created_at && (
<span>: {ComfyuiService.formatDateTime(workflow.created_at)}</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleExecuteWorkflow(workflow)}
className="flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-md hover:bg-green-200 transition-colors"
>
<Play className="w-3 h-3" />
</button>
<button
onClick={() => handleDeleteWorkflow(workflow)}
className="flex items-center gap-1 px-3 py-1 bg-red-100 text-red-700 rounded-md hover:bg-red-200 transition-colors"
>
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* 服务器状态 */}
<div>
<div className="bg-white rounded-lg shadow-sm">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<Server className="w-5 h-5" />
</h2>
</div>
<div className="p-6">
{state.loading.servers ? (
<div className="flex items-center justify-center py-4">
<RefreshCw className="w-5 h-5 animate-spin text-gray-400" />
<span className="ml-2 text-gray-500">...</span>
</div>
) : state.servers.length === 0 ? (
<div className="text-center py-4">
<Server className="w-8 h-8 text-gray-300 mx-auto mb-2" />
<p className="text-gray-500 text-sm"></p>
</div>
) : (
<div className="space-y-3">
{state.servers.map((server) => (
<div
key={server.server_index}
className="border border-gray-200 rounded-lg p-3"
>
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-sm"> {server.server_index}</span>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
ComfyuiService.getServerStatusColor(server) === 'success'
? 'bg-green-100 text-green-700'
: ComfyuiService.getServerStatusColor(server) === 'warning'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}
>
{ComfyuiService.getServerStatusText(server)}
</span>
</div>
<div className="text-xs text-gray-600 space-y-1">
<div>URL: {server.http_url}</div>
{server.is_reachable && (
<div>
队列: 运行 {server.queue_details.running_count} / {server.queue_details.pending_count}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* 模态框组件 */}
<ComfyUIConfigModal
isOpen={showConfigModal}
onClose={() => setShowConfigModal(false)}
currentConfig={state.config}
onConfigUpdate={handleConfigUpdate}
/>
<ComfyUIExecuteModal
isOpen={showExecuteModal}
onClose={() => setShowExecuteModal(false)}
workflow={state.selectedWorkflow || null}
onExecutionComplete={handleExecutionComplete}
/>
<ComfyUIPublishModal
isOpen={showPublishModal}
onClose={() => setShowPublishModal(false)}
onPublishComplete={handlePublishComplete}
/>
</div>
);
};
export default ComfyUIManagement;

View File

@@ -0,0 +1,297 @@
/**
* ComfyUI API 服务层
* 封装所有 ComfyUI API 调用,提供类型安全的前端服务接口
*/
import { invoke } from '@tauri-apps/api/core';
import type {
ComfyuiConfig,
Workflow,
PublishWorkflowRequest,
PublishWorkflowResponse,
DeleteWorkflowResponse,
ExecuteWorkflowRequest,
ExecuteWorkflowResponse,
GetWorkflowSpecRequest,
GetWorkflowSpecResponse,
ServerStatus,
ServerFiles,
ApiRootResponse,
} from '../types/comfyui';
/**
* ComfyUI API 服务类
*/
export class ComfyuiService {
// ============================================================================
// 工作流管理
// ============================================================================
/**
* 获取所有工作流
*/
static async getAllWorkflows(): Promise<Workflow[]> {
try {
const workflows = await invoke<Workflow[]>('comfyui_get_all_workflows');
return workflows;
} catch (error) {
console.error('Failed to get workflows:', error);
throw new Error(`获取工作流列表失败: ${error}`);
}
}
/**
* 发布工作流
*/
static async publishWorkflow(request: PublishWorkflowRequest): Promise<PublishWorkflowResponse> {
try {
const response = await invoke<PublishWorkflowResponse>('comfyui_publish_workflow', { request });
return response;
} catch (error) {
console.error('Failed to publish workflow:', error);
throw new Error(`发布工作流失败: ${error}`);
}
}
/**
* 删除工作流
*/
static async deleteWorkflow(workflowName: string): Promise<DeleteWorkflowResponse> {
try {
const response = await invoke<DeleteWorkflowResponse>('comfyui_delete_workflow', {
workflow_name: workflowName
});
return response;
} catch (error) {
console.error('Failed to delete workflow:', error);
throw new Error(`删除工作流失败: ${error}`);
}
}
// ============================================================================
// 工作流执行
// ============================================================================
/**
* 执行工作流
*/
static async executeWorkflow(request: ExecuteWorkflowRequest): Promise<ExecuteWorkflowResponse> {
try {
const response = await invoke<ExecuteWorkflowResponse>('comfyui_execute_workflow', { request });
return response;
} catch (error) {
console.error('Failed to execute workflow:', error);
throw new Error(`执行工作流失败: ${error}`);
}
}
/**
* 获取工作流规范
*/
static async getWorkflowSpec(request: GetWorkflowSpecRequest): Promise<GetWorkflowSpecResponse> {
try {
const response = await invoke<GetWorkflowSpecResponse>('comfyui_get_workflow_spec', { request });
return response;
} catch (error) {
console.error('Failed to get workflow spec:', error);
throw new Error(`获取工作流规范失败: ${error}`);
}
}
// ============================================================================
// 服务器管理
// ============================================================================
/**
* 获取服务器状态
*/
static async getServersStatus(): Promise<ServerStatus[]> {
try {
const servers = await invoke<ServerStatus[]>('comfyui_get_servers_status');
return servers;
} catch (error) {
console.error('Failed to get servers status:', error);
throw new Error(`获取服务器状态失败: ${error}`);
}
}
/**
* 获取服务器文件列表
*/
static async listServerFiles(serverIndex: number): Promise<ServerFiles> {
try {
const files = await invoke<ServerFiles>('comfyui_list_server_files', {
server_index: serverIndex
});
return files;
} catch (error) {
console.error('Failed to get server files:', error);
throw new Error(`获取服务器文件失败: ${error}`);
}
}
// ============================================================================
// API 信息
// ============================================================================
/**
* 获取 API 根信息
*/
static async getApiRoot(): Promise<ApiRootResponse> {
try {
const apiInfo = await invoke<ApiRootResponse>('comfyui_get_api_root');
return apiInfo;
} catch (error) {
console.error('Failed to get API root:', error);
throw new Error(`获取API信息失败: ${error}`);
}
}
// ============================================================================
// 连接和配置管理
// ============================================================================
/**
* 测试连接
*/
static async testConnection(): Promise<boolean> {
try {
const isConnected = await invoke<boolean>('comfyui_test_connection');
return isConnected;
} catch (error) {
console.error('Failed to test connection:', error);
throw new Error(`连接测试失败: ${error}`);
}
}
/**
* 获取配置
*/
static async getConfig(): Promise<ComfyuiConfig> {
try {
const config = await invoke<ComfyuiConfig>('comfyui_get_config');
return config;
} catch (error) {
console.error('Failed to get config:', error);
throw new Error(`获取配置失败: ${error}`);
}
}
/**
* 更新配置
*/
static async updateConfig(config: ComfyuiConfig): Promise<boolean> {
try {
const success = await invoke<boolean>('comfyui_update_config', { config });
return success;
} catch (error) {
console.error('Failed to update config:', error);
throw new Error(`更新配置失败: ${error}`);
}
}
// ============================================================================
// 辅助方法
// ============================================================================
/**
* 解析工作流名称,提取基础名称和版本
*/
static parseWorkflowName(fullName: string): { baseName: string; version?: string } {
// 工作流名称格式: 'my_workflow [20250101120000]'
const match = fullName.match(/^(.+?)\s*\[(.+?)\]$/);
if (match) {
return {
baseName: match[1].trim(),
version: match[2].trim(),
};
}
return { baseName: fullName };
}
/**
* 格式化工作流名称
*/
static formatWorkflowName(baseName: string, version?: string): string {
if (version) {
return `${baseName} [${version}]`;
}
return baseName;
}
/**
* 验证 JSON 字符串
*/
static validateJson(jsonString: string): { valid: boolean; error?: string } {
try {
JSON.parse(jsonString);
return { valid: true };
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : 'Invalid JSON format'
};
}
}
/**
* 格式化文件大小
*/
static formatFileSize(sizeKb: number): string {
if (sizeKb < 1024) {
return `${sizeKb.toFixed(1)} KB`;
} else if (sizeKb < 1024 * 1024) {
return `${(sizeKb / 1024).toFixed(1)} MB`;
} else {
return `${(sizeKb / (1024 * 1024)).toFixed(1)} GB`;
}
}
/**
* 格式化时间
*/
static formatDateTime(dateString: string): string {
try {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
} catch (error) {
return dateString;
}
}
/**
* 获取服务器状态显示文本
*/
static getServerStatusText(server: ServerStatus): string {
if (!server.is_reachable) {
return '不可达';
}
if (server.is_free) {
return '空闲';
}
const { running_count, pending_count } = server.queue_details;
return `运行中: ${running_count}, 等待: ${pending_count}`;
}
/**
* 获取服务器状态颜色
*/
static getServerStatusColor(server: ServerStatus): 'success' | 'warning' | 'error' {
if (!server.is_reachable) {
return 'error';
}
if (server.is_free) {
return 'success';
}
return 'warning';
}
}
export default ComfyuiService;

View File

@@ -0,0 +1,324 @@
/**
* ComfyUI API 类型定义
* 与后端 Rust 数据模型保持一致
*/
// ============================================================================
// 配置相关类型
// ============================================================================
/**
* ComfyUI API 服务配置
*/
export interface ComfyuiConfig {
/** API 基础 URL */
base_url: string;
/** 请求超时时间(秒) */
timeout?: number;
/** 重试次数 */
retry_attempts?: number;
/** 是否启用缓存 */
enable_cache?: boolean;
/** 最大并发数 */
max_concurrency?: number;
}
// ============================================================================
// 工作流相关类型
// ============================================================================
/**
* 工作流对象
*/
export interface Workflow {
/** 工作流的完整唯一名称,例如 'my_workflow [20250101120000]' */
name: string;
/** 工作流的基础名称 */
base_name?: string;
/** 工作流版本 */
version?: string;
/** 工作流创建时间 */
created_at?: string;
/** 工作流更新时间 */
updated_at?: string;
/** 工作流描述 */
description?: string;
/** 工作流配置数据 */
[key: string]: any;
}
/**
* 发布工作流请求
*/
export interface PublishWorkflowRequest {
/** 工作流名称 */
name: string;
/** 工作流配置数据 */
workflow_data: any;
/** 工作流描述 */
description?: string;
/** 工作流版本 */
version?: string;
}
/**
* 发布工作流响应
*/
export interface PublishWorkflowResponse {
/** 操作是否成功 */
success: boolean;
/** 响应消息 */
message?: string;
/** 创建的工作流信息 */
workflow?: Workflow;
}
/**
* 删除工作流响应
*/
export interface DeleteWorkflowResponse {
/** 操作是否成功 */
success: boolean;
/** 响应消息 */
message?: string;
/** 删除的工作流名称 */
deleted_workflow?: string;
}
// ============================================================================
// 执行相关类型
// ============================================================================
/**
* 执行工作流请求
*/
export interface ExecuteWorkflowRequest {
/** 工作流基础名称 */
base_name: string;
/** 工作流版本(可选) */
version?: string;
/** 请求数据 */
request_data: any;
}
/**
* 执行工作流响应
*/
export interface ExecuteWorkflowResponse {
/** 执行任务ID */
task_id?: string;
/** 执行状态 */
status?: string;
/** 响应消息 */
message?: string;
/** 执行结果数据 */
result?: any;
/** 错误信息 */
error?: string;
}
/**
* 获取工作流规范请求
*/
export interface GetWorkflowSpecRequest {
/** 工作流基础名称 */
base_name: string;
/** 工作流版本(可选) */
version?: string;
}
/**
* 获取工作流规范响应
*/
export interface GetWorkflowSpecResponse {
/** 工作流规范数据 */
spec: any;
/** 工作流名称 */
name?: string;
/** 工作流版本 */
version?: string;
/** 规范描述 */
description?: string;
}
// ============================================================================
// 服务器相关类型
// ============================================================================
/**
* 服务器队列详情
*/
export interface ServerQueueDetails {
/** 正在运行的任务数量 */
running_count: number;
/** 等待中的任务数量 */
pending_count: number;
}
/**
* 服务器状态信息
*/
export interface ServerStatus {
/** 服务器在配置列表中的索引 */
server_index: number;
/** HTTP URL */
http_url: string;
/** WebSocket URL */
ws_url: string;
/** 输入目录路径 */
input_dir: string;
/** 输出目录路径 */
output_dir: string;
/** 服务器是否可达 */
is_reachable: boolean;
/** 服务器是否空闲 */
is_free: boolean;
/** 队列详情 */
queue_details: ServerQueueDetails;
}
/**
* 文件详情
*/
export interface FileDetails {
/** 文件名 */
name: string;
/** 文件大小KB */
size_kb: number;
/** 修改时间 */
modified_at: string;
}
/**
* 服务器文件信息
*/
export interface ServerFiles {
/** 服务器在配置列表中的索引 */
server_index: number;
/** HTTP URL */
http_url: string;
/** 输入文件列表 */
input_files: FileDetails[];
/** 输出文件列表 */
output_files: FileDetails[];
}
// ============================================================================
// API 相关类型
// ============================================================================
/**
* API 根响应
*/
export interface ApiRootResponse {
/** API 使用指南 */
guide: string;
/** API 版本 */
version?: string;
/** 可用端点 */
endpoints?: string[];
}
/**
* HTTP 验证错误详情
*/
export interface ValidationError {
/** 错误位置 */
loc: any[];
/** 错误消息 */
msg: string;
/** 错误类型 */
type: string;
}
/**
* HTTP 验证错误响应
*/
export interface HTTPValidationError {
/** 错误详情列表 */
detail: ValidationError[];
}
// ============================================================================
// 错误类型
// ============================================================================
/**
* ComfyUI API 错误类型
*/
export type ComfyuiError =
| { type: 'NetworkError'; message: string }
| { type: 'HttpError'; status: number; message: string }
| { type: 'ValidationError'; errors: ValidationError[] }
| { type: 'ServerError'; message: string }
| { type: 'WorkflowNotFound'; workflow_name: string }
| { type: 'ConfigError'; message: string }
| { type: 'TimeoutError'; message: string }
| { type: 'UnknownError'; message: string };
// ============================================================================
// UI 状态类型
// ============================================================================
/**
* 工作流执行状态
*/
export type WorkflowExecutionStatus = 'idle' | 'running' | 'success' | 'error';
/**
* 服务器连接状态
*/
export type ServerConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error';
/**
* 工作流管理 UI 状态
*/
export interface ComfyuiUIState {
/** 工作流列表 */
workflows: Workflow[];
/** 选中的工作流 */
selectedWorkflow?: Workflow;
/** 服务器状态列表 */
servers: ServerStatus[];
/** 当前配置 */
config: ComfyuiConfig;
/** 连接状态 */
connectionStatus: ServerConnectionStatus;
/** 执行状态 */
executionStatus: WorkflowExecutionStatus;
/** 加载状态 */
loading: {
workflows: boolean;
servers: boolean;
execution: boolean;
config: boolean;
};
/** 错误信息 */
error?: string;
}
/**
* 工作流表单数据
*/
export interface WorkflowFormData {
/** 工作流名称 */
name: string;
/** 工作流描述 */
description: string;
/** 工作流版本 */
version: string;
/** 工作流配置数据 */
workflow_data: string; // JSON 字符串
}
/**
* 执行工作流表单数据
*/
export interface ExecuteWorkflowFormData {
/** 工作流基础名称 */
base_name: string;
/** 工作流版本 */
version?: string;
/** 请求数据 */
request_data: string; // JSON 字符串
}