feat: add workflow studio and postgres support
This commit is contained in:
+93
-135
@@ -1,16 +1,20 @@
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getSessionToken } from '../authSession.js';
|
||||
|
||||
export default function ComfyUI() {
|
||||
const [isDark, setIsDark] = React.useState(true);
|
||||
const [iframeSrc, setIframeSrc] = React.useState('');
|
||||
const [starting, setStarting] = React.useState(true);
|
||||
const [message, setMessage] = React.useState('正在启动 ComfyUI...');
|
||||
const [error, setError] = React.useState('');
|
||||
const location = useLocation();
|
||||
const audience = location.pathname.startsWith('/user/') ? 'user' : 'admin';
|
||||
const token = getSessionToken(audience, localStorage);
|
||||
const [available, setAvailable] = React.useState(false);
|
||||
const [message, setMessage] = React.useState('正在检查外部 ComfyUI...');
|
||||
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
|
||||
|
||||
const openFullscreen = () => {
|
||||
window.open('/api/comfyui/proxy/', '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const reloadFrame = () => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = '/api/comfyui/proxy/';
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = document.documentElement;
|
||||
@@ -23,121 +27,25 @@ export default function ComfyUI() {
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const encodedToken = token ? encodeURIComponent(token) : '';
|
||||
const requestTimeoutMs = 35_000;
|
||||
|
||||
const fetchWithTimeout = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const controller = new AbortController();
|
||||
const timer = window.setTimeout(() => controller.abort(), requestTimeoutMs);
|
||||
try {
|
||||
return await fetch(input, { ...init, signal: controller.signal });
|
||||
} finally {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveIframeFromPayload = (payload: any) => {
|
||||
const comfyui = payload?.comfyui || payload || {};
|
||||
const mode = String(comfyui?.mode || '').toLowerCase();
|
||||
const status = String(comfyui?.status || '').toLowerCase();
|
||||
const lastError = String(comfyui?.lastError || comfyui?.last_error || '').trim();
|
||||
|
||||
if (mode === 'container') {
|
||||
if (status === 'running') return `/comfy/?token=${encodedToken}`;
|
||||
if (status === 'unhealthy' || status === 'failed') {
|
||||
throw new Error(lastError || 'ComfyUI 容器启动失败');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
if (status && status !== 'disabled') {
|
||||
return '/api/comfyui/proxy/';
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
throw new Error(lastError);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
setIframeSrc('');
|
||||
setStarting(false);
|
||||
setMessage('登录状态已过期,请重新登录。');
|
||||
setError('');
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
setStarting(true);
|
||||
setIframeSrc('');
|
||||
setError('');
|
||||
setMessage('正在启动 ComfyUI...');
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
const response = await fetchWithTimeout('/api/video-agent/start', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!cancelled) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`启动接口返回 ${response.status}`);
|
||||
}
|
||||
const payload = await response.json().catch(() => null);
|
||||
const nextSrc = resolveIframeFromPayload(payload);
|
||||
if (nextSrc) {
|
||||
setIframeSrc(nextSrc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (!cancelled) {
|
||||
setMessage('ComfyUI 启动较慢,正在检查状态...');
|
||||
if (e?.name !== 'AbortError') setError(e?.message || '启动请求失败');
|
||||
}
|
||||
}
|
||||
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (!cancelled && Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetchWithTimeout('/api/video-agent/status', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
const payload = await response.json().catch(() => null);
|
||||
const nextSrc = resolveIframeFromPayload(payload);
|
||||
if (nextSrc) {
|
||||
setIframeSrc(nextSrc);
|
||||
return;
|
||||
}
|
||||
setMessage('ComfyUI 仍在启动,等待容器就绪...');
|
||||
} else {
|
||||
setError(`状态接口返回 ${response.status}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e?.name === 'AbortError' ? '状态检查超时' : (e?.message || '状态检查失败'));
|
||||
}
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setStarting(false);
|
||||
setMessage('ComfyUI 启动超时');
|
||||
setError((current) => current || '请检查 /api/video-agent/status、Docker 容器日志和 4000 后端进程。');
|
||||
}
|
||||
};
|
||||
|
||||
void start().finally(() => {
|
||||
if (!cancelled) setStarting(false);
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
void fetch('/api/comfyui/proxy/', { method: 'GET', signal: controller.signal })
|
||||
.then((response) => {
|
||||
if (cancelled) return;
|
||||
setAvailable(response.ok);
|
||||
setMessage(response.ok
|
||||
? ''
|
||||
: '外部 ComfyUI 不可达。请在 config.toml 中配置 comfyui.base_url,或使用工作流。');
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled || err?.name === 'AbortError') return;
|
||||
setAvailable(false);
|
||||
setMessage('外部 ComfyUI 不可达。请在 config.toml 中配置 comfyui.base_url,或使用工作流。');
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
};
|
||||
}, [token]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -149,15 +57,23 @@ export default function ComfyUI() {
|
||||
background: isDark ? '#1a1a2e' : '#f5f5f5',
|
||||
}}
|
||||
>
|
||||
{iframeSrc ? (
|
||||
<div style={toolbarStyle}>
|
||||
<div style={toolbarTitleStyle}>外部 ComfyUI</div>
|
||||
<div style={toolbarActionsStyle}>
|
||||
<button type="button" onClick={reloadFrame} disabled={!available} style={toolbarButtonStyle}>
|
||||
刷新
|
||||
</button>
|
||||
<button type="button" onClick={openFullscreen} disabled={!available} style={primaryToolbarButtonStyle}>
|
||||
新窗口打开
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{available ? (
|
||||
<iframe
|
||||
src={iframeSrc}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
title="ComfyUI"
|
||||
ref={iframeRef}
|
||||
src="/api/comfyui/proxy/"
|
||||
style={{ width: '100%', height: '100%', border: 'none' }}
|
||||
title="External ComfyUI"
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
) : (
|
||||
@@ -168,16 +84,58 @@ export default function ComfyUI() {
|
||||
color: 'var(--color-text-muted)',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
gap: 8,
|
||||
padding: 24,
|
||||
}}>
|
||||
<div>{starting ? message : message}</div>
|
||||
{error && (
|
||||
<div style={{ color: '#ef4444', fontSize: 12, maxWidth: 520 }}>
|
||||
{error}
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, color: 'var(--color-text-primary)', marginBottom: 8 }}>
|
||||
内置 Docker ComfyUI 工作台已移除
|
||||
</div>
|
||||
)}
|
||||
<div style={{ maxWidth: 560, lineHeight: 1.7 }}>{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const toolbarStyle: React.CSSProperties = {
|
||||
minHeight: 44,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
padding: '8px 12px',
|
||||
borderBottom: '1px solid var(--color-border-light)',
|
||||
background: 'var(--color-surface)',
|
||||
};
|
||||
|
||||
const toolbarTitleStyle: React.CSSProperties = {
|
||||
fontSize: 14,
|
||||
fontWeight: 800,
|
||||
color: 'var(--color-text-primary)',
|
||||
};
|
||||
|
||||
const toolbarActionsStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
};
|
||||
|
||||
const toolbarButtonStyle: React.CSSProperties = {
|
||||
minHeight: 30,
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 8,
|
||||
padding: '5px 10px',
|
||||
background: 'var(--color-bg)',
|
||||
color: 'var(--color-text-primary)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
};
|
||||
|
||||
const primaryToolbarButtonStyle: React.CSSProperties = {
|
||||
...toolbarButtonStyle,
|
||||
borderColor: 'var(--color-primary)',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
fontWeight: 700,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user