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:
@@ -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 />} />
|
||||
|
||||
358
apps/desktop/src/components/ComfyUIExecuteModal.tsx
Normal file
358
apps/desktop/src/components/ComfyUIExecuteModal.tsx
Normal 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;
|
||||
361
apps/desktop/src/components/ComfyUIPublishModal.tsx
Normal file
361
apps/desktop/src/components/ComfyUIPublishModal.tsx
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
448
apps/desktop/src/pages/ComfyUIManagement.tsx
Normal file
448
apps/desktop/src/pages/ComfyUIManagement.tsx
Normal 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;
|
||||
297
apps/desktop/src/services/comfyuiService.ts
Normal file
297
apps/desktop/src/services/comfyuiService.ts
Normal 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;
|
||||
324
apps/desktop/src/types/comfyui.ts
Normal file
324
apps/desktop/src/types/comfyui.ts
Normal 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 字符串
|
||||
}
|
||||
Reference in New Issue
Block a user