fix: enable multi tenant comfy workspace
This commit is contained in:
+11
-4
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+85
-29
@@ -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) {
|
||||||
const payload = await response.json().catch(() => null);
|
if (!response.ok) {
|
||||||
const mode = String(payload?.comfyui?.mode || '').toLowerCase();
|
throw new Error(`启动接口返回 ${response.status}`);
|
||||||
const status = String(payload?.comfyui?.status || '').toLowerCase();
|
|
||||||
const usesContainerProxy = mode === 'container';
|
|
||||||
|
|
||||||
if (usesContainerProxy) {
|
|
||||||
setIframeSrc(`/comfy/?token=${encodedToken}`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
if (status !== 'disabled') {
|
const nextSrc = resolveIframeFromPayload(payload);
|
||||||
setIframeSrc('/api/comfyui/proxy/');
|
if (nextSrc) {
|
||||||
|
setIframeSrc(nextSrc);
|
||||||
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 || '启动请求失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const deadline = Date.now() + 120_000;
|
||||||
const response = await fetch('/api/video-agent/status', {
|
while (!cancelled && Date.now() < deadline) {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
try {
|
||||||
});
|
const response = await fetchWithTimeout('/api/video-agent/status', {
|
||||||
if (!cancelled && response.ok) {
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
const payload = await response.json().catch(() => null);
|
});
|
||||||
const mode = String(payload?.mode || payload?.comfyui?.mode || '').toLowerCase();
|
if (response.ok) {
|
||||||
if (mode === 'container') {
|
const payload = await response.json().catch(() => null);
|
||||||
setIframeSrc(`/comfy/?token=${encodedToken}`);
|
const nextSrc = resolveIframeFromPayload(payload);
|
||||||
return;
|
if (nextSrc) {
|
||||||
|
setIframeSrc(nextSrc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMessage('ComfyUI 仍在启动,等待容器就绪...');
|
||||||
|
} else {
|
||||||
|
setError(`状态接口返回 ${response.status}`);
|
||||||
}
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e?.name === 'AbortError' ? '状态检查超时' : (e?.message || '状态检查失败'));
|
||||||
}
|
}
|
||||||
} catch {
|
await new Promise((resolve) => window.setTimeout(resolve, 2000));
|
||||||
// Fall through to the legacy embedded ComfyUI proxy.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user