feat: auto-redirect to /connect when backend unreachable, add comfy-cli guide

When deployed to static hosting (Cloudflare Pages), the frontend now
detects that no backend is available and redirects to /connect instead
of hanging on "Loading ComfyUI". The connection panel includes comfy-cli
quick start guide, connection tester, and "Connect & Go" button.
API requests are routed to the user-configured remote backend URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
snomiao
2026-04-11 04:15:33 +09:00
parent 0d5ced4686
commit b4f8605ab2
5 changed files with 133 additions and 18 deletions

View File

@@ -3764,9 +3764,15 @@
"ws": "WS",
"status": "Connection Status",
"connected": "Connected — backend is reachable.",
"guide": "How to Run ComfyUI",
"guideDescription": "Start ComfyUI with CORS enabled so this preview can connect from a different origin:",
"corsNote": "Replace \"*\" with this page's URL for tighter security.",
"connectAndGo": "Connect & Open ComfyUI",
"quickStart": "Quick Start with Comfy CLI",
"quickStartDescription": "The fastest way to get ComfyUI running locally:",
"step1Install": "1. Install comfy-cli:",
"step2Install": "2. Install ComfyUI:",
"step3Launch": "3. Launch with CORS enabled:",
"altManualSetup": "Alternative: Manual Python Setup",
"guideDescription": "If you already have ComfyUI cloned, start it with CORS enabled:",
"corsNote": "The --enable-cors-header flag allows this preview page to communicate with your local backend.",
"localAccess": "Local Network Access",
"localAccessDescription": "Your browser may prompt for permission to access local network devices. Allow it so this page can reach your local ComfyUI instance.",
"source": "Source",

View File

@@ -66,6 +66,24 @@ const router = createRouter({
name: 'GraphView',
component: () => import('@/views/GraphView.vue'),
beforeEnter: async (_to, _from, next) => {
// Check if backend is reachable before loading the graph view
const backendUrl =
localStorage.getItem('comfyui-preview-backend-url') || ''
const apiBase = backendUrl ? backendUrl.replace(/\/+$/, '') : ''
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 3000)
const res = await fetch(`${apiBase}/api/system_stats`, {
signal: controller.signal
})
clearTimeout(timeout)
if (!res.ok || !(await res.json()).system) {
return next('/connect')
}
} catch {
return next('/connect')
}
// Then check user store
const userStore = useUserStore()
await userStore.initialize()

View File

@@ -370,10 +370,19 @@ export class ComfyApi extends EventTarget {
constructor() {
super()
this.user = ''
this.api_host = location.host
this.api_base = isCloud
? ''
: location.pathname.split('/').slice(0, -1).join('/')
const remoteBackend = localStorage.getItem('comfyui-preview-backend-url')
if (remoteBackend) {
const url = new URL(remoteBackend)
this.api_host = url.host
this.api_base = url.origin + url.pathname.replace(/\/+$/, '')
} else {
this.api_host = location.host
this.api_base = isCloud
? ''
: location.pathname.split('/').slice(0, -1).join('/')
}
this.initialClientId = sessionStorage.getItem('clientId')
}

View File

@@ -13,6 +13,12 @@ vi.mock('@/platform/distribution/types', () => ({
isDesktop: false
}))
vi.mock('vue-router', () => ({
useRouter: () => ({
push: vi.fn()
})
}))
const mockLocalStorage = vi.hoisted(() => {
const store: Record<string, string> = {}
return {
@@ -83,9 +89,14 @@ describe('ConnectionPanelView', () => {
expect(buttons.length).toBeGreaterThan(0)
})
it('displays the command-line guide', () => {
it('displays the comfy-cli install command', () => {
const wrapper = mountPanel()
expect(wrapper.text()).toContain('python main.py --enable-cors-header="*"')
expect(wrapper.text()).toContain('pip install comfy-cli')
})
it('displays the comfy launch command', () => {
const wrapper = mountPanel()
expect(wrapper.text()).toContain('comfy launch -- --enable-cors-header="*"')
})
it('shows build info in footer', () => {

View File

@@ -12,6 +12,7 @@
</p>
</header>
<!-- Backend URL input -->
<section class="flex flex-col gap-2">
<label for="backend-url" class="text-sm font-medium text-neutral-300">
{{ t('connectionPanel.backendUrl') }}
@@ -37,6 +38,7 @@
</div>
</section>
<!-- Connection status -->
<section
v-if="httpStatus !== null || wsStatus !== null"
class="flex flex-col gap-2 rounded-md bg-neutral-800/50 p-3"
@@ -85,25 +87,88 @@
>
{{ t('connectionPanel.connected') }}
</p>
<!-- Connect & Go button -->
<Button
v-if="httpStatus === true"
variant="primary"
size="lg"
class="mt-2 w-full"
@click="connectAndGo"
>
{{ t('connectionPanel.connectAndGo') }}
</Button>
</section>
<section class="flex flex-col gap-2">
<!-- Quick Start with Comfy CLI -->
<section class="flex flex-col gap-3">
<h2 class="text-sm font-medium text-neutral-300">
{{ t('connectionPanel.guide') }}
{{ t('connectionPanel.quickStart') }}
</h2>
<p class="text-xs text-neutral-400">
{{ t('connectionPanel.guideDescription') }}
{{ t('connectionPanel.quickStartDescription') }}
</p>
<code
class="block rounded-md bg-neutral-800 p-3 text-xs text-neutral-200 select-all"
>
python main.py --enable-cors-header="*"
</code>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-neutral-400">
{{ t('connectionPanel.step1Install') }}
</span>
<code
class="block rounded-md bg-neutral-800 p-3 text-xs text-neutral-200 select-all"
>
pip install comfy-cli
</code>
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-neutral-400">
{{ t('connectionPanel.step2Install') }}
</span>
<code
class="block rounded-md bg-neutral-800 p-3 text-xs text-neutral-200 select-all"
>
comfy install
</code>
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-neutral-400">
{{ t('connectionPanel.step3Launch') }}
</span>
<code
class="block rounded-md bg-neutral-800 p-3 text-xs text-neutral-200 select-all"
>
comfy launch -- --enable-cors-header="*"
</code>
</div>
</div>
<p class="text-xs text-neutral-500">
{{ t('connectionPanel.corsNote') }}
</p>
</section>
<!-- Alternative: manual python -->
<details class="group">
<summary
class="cursor-pointer text-sm font-medium text-neutral-400 hover:text-neutral-300"
>
{{ t('connectionPanel.altManualSetup') }}
</summary>
<div class="mt-2 flex flex-col gap-2">
<p class="text-xs text-neutral-400">
{{ t('connectionPanel.guideDescription') }}
</p>
<code
class="block rounded-md bg-neutral-800 p-3 text-xs text-neutral-200 select-all"
>
python main.py --enable-cors-header="*"
</code>
</div>
</details>
<!-- Local network access -->
<section class="flex flex-col gap-2">
<h2 class="text-sm font-medium text-neutral-300">
{{ t('connectionPanel.localAccess') }}
@@ -138,7 +203,6 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
@@ -230,6 +294,13 @@ async function testConnection() {
}
}
function connectAndGo() {
const base = normalizeUrl(backendUrl.value)
localStorage.setItem(STORAGE_KEY, base)
// Full page reload so ComfyApi constructor picks up the new backend URL
window.location.href = '/'
}
const version = __COMFYUI_FRONTEND_VERSION__
const commit = __COMFYUI_FRONTEND_COMMIT__
const branch = __CI_BRANCH__