fix: enable multi tenant comfy workspace

This commit is contained in:
boos
2026-05-22 00:44:57 +08:00
parent 5ef3f8aea8
commit 7c6364a160
3 changed files with 106 additions and 35 deletions
+11 -4
View File
@@ -13,6 +13,7 @@ FRONTEND_ONLY=0
BACKEND_ONLY=0 BACKEND_ONLY=0
NO_RUN=0 NO_RUN=0
REBUILD_IMAGE=0 REBUILD_IMAGE=0
CARGO_FEATURES="${BOOSAPI_CARGO_FEATURES:-multi_tenant_comfy}"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -24,6 +25,7 @@ usage() {
--backend-only 只构建后端 --backend-only 只构建后端
--no-run 构建后不启动服务 --no-run 构建后不启动服务
--rebuild-image 强制重新构建多租户 ComfyUI 镜像(默认:已存在则跳过) --rebuild-image 强制重新构建多租户 ComfyUI 镜像(默认:已存在则跳过)
BOOSAPI_CARGO_FEATURES 可覆盖后端 Cargo features(默认:multi_tenant_comfy)
-h, --help 显示帮助 -h, --help 显示帮助
EOF EOF
} }
@@ -173,14 +175,19 @@ if [ "$FRONTEND_ONLY" -eq 0 ]; then
log "提示: 安装 mold 后取消 .cargo/config.toml 中的相应注释,链接将快 3-5x" log "提示: 安装 mold 后取消 .cargo/config.toml 中的相应注释,链接将快 3-5x"
fi fi
CARGO_FEATURE_ARGS=()
if [ -n "$CARGO_FEATURES" ]; then
CARGO_FEATURE_ARGS=(--features "$CARGO_FEATURES")
fi
if [ "$RELEASE" -eq 1 ]; then if [ "$RELEASE" -eq 1 ]; then
log "构建后端 (cargo build --release)..." log "构建后端 (cargo build --release ${CARGO_FEATURE_ARGS[*]})..."
cargo build --release cargo build --release "${CARGO_FEATURE_ARGS[@]}"
BINARY="$ROOT_DIR/target/release/boosapi" BINARY="$ROOT_DIR/target/release/boosapi"
ok "后端构建完成 → target/release/boosapi" ok "后端构建完成 → target/release/boosapi"
else else
log "构建后端 (cargo build)..." log "构建后端 (cargo build ${CARGO_FEATURE_ARGS[*]})..."
cargo build cargo build "${CARGO_FEATURE_ARGS[@]}"
BINARY="$ROOT_DIR/target/debug/boosapi" BINARY="$ROOT_DIR/target/debug/boosapi"
ok "后端构建完成 → target/debug/boosapi" ok "后端构建完成 → target/debug/boosapi"
fi fi
+10 -2
View File
@@ -189,7 +189,11 @@ impl ComfyUIPool {
/// handle. Idempotent. /// handle. Idempotent.
pub async fn get_or_start(&self, user_id: i64) -> PoolResult<PoolEntry> { pub async fn get_or_start(&self, user_id: i64) -> PoolResult<PoolEntry> {
// Fast path: already ready. // Fast path: already ready.
if let Some(entry) = self.containers.read().await.get(&user_id).cloned() { let existing = {
let map = self.containers.read().await;
map.get(&user_id).cloned()
};
if let Some(entry) = existing {
if entry.state == ContainerState::Ready { if entry.state == ContainerState::Ready {
self.touch_inner(user_id).await; self.touch_inner(user_id).await;
return Ok(entry); return Ok(entry);
@@ -206,7 +210,11 @@ impl ComfyUIPool {
let _g = lock.lock().await; let _g = lock.lock().await;
// Re-check after acquiring the lock; another task may have done the work. // Re-check after acquiring the lock; another task may have done the work.
if let Some(entry) = self.containers.read().await.get(&user_id).cloned() { let existing = {
let map = self.containers.read().await;
map.get(&user_id).cloned()
};
if let Some(entry) = existing {
if entry.state == ContainerState::Ready { if entry.state == ContainerState::Ready {
self.touch_inner(user_id).await; self.touch_inner(user_id).await;
return Ok(entry); return Ok(entry);
+80 -24
View File
@@ -6,6 +6,8 @@ export default function ComfyUI() {
const [isDark, setIsDark] = React.useState(true); const [isDark, setIsDark] = React.useState(true);
const [iframeSrc, setIframeSrc] = React.useState(''); const [iframeSrc, setIframeSrc] = React.useState('');
const [starting, setStarting] = React.useState(true); const [starting, setStarting] = React.useState(true);
const [message, setMessage] = React.useState('正在启动 ComfyUI...');
const [error, setError] = React.useState('');
const location = useLocation(); const location = useLocation();
const audience = location.pathname.startsWith('/user/') ? 'user' : 'admin'; const audience = location.pathname.startsWith('/user/') ? 'user' : 'admin';
const token = getSessionToken(audience, localStorage); const token = getSessionToken(audience, localStorage);
@@ -22,10 +24,48 @@ export default function ComfyUI() {
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
const encodedToken = token ? encodeURIComponent(token) : ''; 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) { if (!token) {
setIframeSrc(''); setIframeSrc('');
setStarting(false); setStarting(false);
setMessage('登录状态已过期,请重新登录。');
setError('');
return () => { return () => {
cancelled = true; cancelled = true;
}; };
@@ -33,51 +73,60 @@ export default function ComfyUI() {
setStarting(true); setStarting(true);
setIframeSrc(''); setIframeSrc('');
setError('');
setMessage('正在启动 ComfyUI...');
const start = async () => { const start = async () => {
try { try {
const response = await fetch('/api/video-agent/start', { const response = await fetchWithTimeout('/api/video-agent/start', {
method: 'POST', method: 'POST',
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (!cancelled && response.ok) { if (!cancelled) {
if (!response.ok) {
throw new Error(`启动接口返回 ${response.status}`);
}
const payload = await response.json().catch(() => null); const payload = await response.json().catch(() => null);
const mode = String(payload?.comfyui?.mode || '').toLowerCase(); const nextSrc = resolveIframeFromPayload(payload);
const status = String(payload?.comfyui?.status || '').toLowerCase(); if (nextSrc) {
const usesContainerProxy = mode === 'container'; setIframeSrc(nextSrc);
if (usesContainerProxy) {
setIframeSrc(`/comfy/?token=${encodedToken}`);
return;
}
if (status !== 'disabled') {
setIframeSrc('/api/comfyui/proxy/');
return; return;
} }
} }
} catch { } catch (e: any) {
// Fall through to the legacy embedded ComfyUI proxy. if (!cancelled) {
setMessage('ComfyUI 启动较慢,正在检查状态...');
if (e?.name !== 'AbortError') setError(e?.message || '启动请求失败');
}
} }
const deadline = Date.now() + 120_000;
while (!cancelled && Date.now() < deadline) {
try { try {
const response = await fetch('/api/video-agent/status', { const response = await fetchWithTimeout('/api/video-agent/status', {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}); });
if (!cancelled && response.ok) { if (response.ok) {
const payload = await response.json().catch(() => null); const payload = await response.json().catch(() => null);
const mode = String(payload?.mode || payload?.comfyui?.mode || '').toLowerCase(); const nextSrc = resolveIframeFromPayload(payload);
if (mode === 'container') { if (nextSrc) {
setIframeSrc(`/comfy/?token=${encodedToken}`); setIframeSrc(nextSrc);
return; return;
} }
setMessage('ComfyUI 仍在启动,等待容器就绪...');
} else {
setError(`状态接口返回 ${response.status}`);
} }
} catch { } catch (e: any) {
// Fall through to the legacy embedded ComfyUI proxy. setError(e?.name === 'AbortError' ? '状态检查超时' : (e?.message || '状态检查失败'));
}
await new Promise((resolve) => window.setTimeout(resolve, 2000));
} }
if (!cancelled) { if (!cancelled) {
setIframeSrc('/api/comfyui/proxy/'); setStarting(false);
setMessage('ComfyUI 启动超时');
setError((current) => current || '请检查 /api/video-agent/status、Docker 容器日志和 4000 后端进程。');
} }
}; };
@@ -118,8 +167,15 @@ export default function ComfyUI() {
placeItems: 'center', placeItems: 'center',
color: 'var(--color-text-muted)', color: 'var(--color-text-muted)',
fontSize: 14, fontSize: 14,
textAlign: 'center',
gap: 8,
}}> }}>
{starting ? '正在启动 ComfyUI...' : '登录状态已过期,请重新登录。'} <div>{starting ? message : message}</div>
{error && (
<div style={{ color: '#ef4444', fontSize: 12, maxWidth: 520 }}>
{error}
</div>
)}
</div> </div>
)} )}
</div> </div>