mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-11 00:38:37 +00:00
Compare commits
1 Commits
uy/node-se
...
account-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a564b48787 |
@@ -2963,8 +2963,18 @@
|
||||
"technicalDetails": "Technical Details",
|
||||
"helpText": "Need help? Contact",
|
||||
"supportLink": "support"
|
||||
},
|
||||
"banned": {
|
||||
"title": "Your account has been banned",
|
||||
"message": "Your account has been banned for misuse. If you believe this is a mistake, please contact support.",
|
||||
"contactSupport": "Contact support",
|
||||
"signOut": "Sign out"
|
||||
}
|
||||
},
|
||||
"accountBanned": {
|
||||
"toastSummary": "Account banned",
|
||||
"toastDetail": "Your account has been banned for misuse. Please contact support@comfy.org if you believe this is a mistake."
|
||||
},
|
||||
"cloudFooter_needHelp": "Need Help?",
|
||||
"cloudStart_title": "start creating in seconds",
|
||||
"cloudStart_desc": "Zero setup required. Works on any device.",
|
||||
|
||||
25
src/main.ts
25
src/main.ts
@@ -12,6 +12,8 @@ import { createApp } from 'vue'
|
||||
import { VueFire, VueFireAuth } from 'vuefire'
|
||||
|
||||
import { setAssertReporter } from '@/base/assert'
|
||||
import { onAccountBanned } from '@/platform/auth/accountBanned'
|
||||
import { installAccountBannedFetchInterceptor } from '@/platform/auth/accountBannedInterceptors'
|
||||
import { getFirebaseConfig } from '@/config/firebase'
|
||||
import { flushProxyWidgetMigration } from '@/core/graph/subgraph/migration/proxyWidgetMigration'
|
||||
import { autoExposeKnownPreviewNodes } from '@/core/graph/subgraph/promotionUtils'
|
||||
@@ -29,7 +31,7 @@ import { useBootstrapStore } from '@/stores/bootstrapStore'
|
||||
import App from './App.vue'
|
||||
// Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css)
|
||||
import './assets/css/style.css'
|
||||
import { i18n } from './i18n'
|
||||
import { i18n, t } from './i18n'
|
||||
|
||||
/**
|
||||
* CRITICAL: Load remote config FIRST for cloud builds to ensure
|
||||
@@ -145,6 +147,27 @@ LGraph.proxyWidgetMigrationFlush = (hostNode, nodeData) =>
|
||||
LGraph.autoExposePreviewNodes = (hostNode) =>
|
||||
autoExposeKnownPreviewNodes(hostNode)
|
||||
|
||||
installAccountBannedFetchInterceptor()
|
||||
|
||||
let accountBannedHandled = false
|
||||
onAccountBanned(() => {
|
||||
if (accountBannedHandled) return
|
||||
accountBannedHandled = true
|
||||
|
||||
if (isCloud) {
|
||||
if (router.currentRoute.value.name !== 'cloud-banned') {
|
||||
void router.replace({ name: 'cloud-banned' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
useToastStore(pinia).add({
|
||||
severity: 'error',
|
||||
summary: t('accountBanned.toastSummary'),
|
||||
detail: t('accountBanned.toastDetail')
|
||||
})
|
||||
})
|
||||
|
||||
const bootstrapStore = useBootstrapStore(pinia)
|
||||
void bootstrapStore.startStoreBootstrap()
|
||||
|
||||
|
||||
45
src/platform/auth/accountBanned.test.ts
Normal file
45
src/platform/auth/accountBanned.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
isAccountBannedResponseBody,
|
||||
notifyAccountBanned,
|
||||
onAccountBanned
|
||||
} from '@/platform/auth/accountBanned'
|
||||
|
||||
describe('isAccountBannedResponseBody', () => {
|
||||
it('is true when the body carries the ACCOUNT_BANNED code', () => {
|
||||
expect(isAccountBannedResponseBody({ code: 'ACCOUNT_BANNED' })).toBe(true)
|
||||
})
|
||||
|
||||
it('is false for an ordinary access-denied body', () => {
|
||||
expect(isAccountBannedResponseBody({ code: 'ACCESS_DENIED' })).toBe(false)
|
||||
})
|
||||
|
||||
it('is false for non-object bodies', () => {
|
||||
expect(isAccountBannedResponseBody(null)).toBe(false)
|
||||
expect(isAccountBannedResponseBody('ACCOUNT_BANNED')).toBe(false)
|
||||
expect(isAccountBannedResponseBody(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('account banned subscription', () => {
|
||||
it('invokes subscribed listeners on notify', () => {
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = onAccountBanned(listener)
|
||||
|
||||
notifyAccountBanned()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('stops invoking a listener after it unsubscribes', () => {
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = onAccountBanned(listener)
|
||||
|
||||
unsubscribe()
|
||||
notifyAccountBanned()
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
39
src/platform/auth/accountBanned.ts
Normal file
39
src/platform/auth/accountBanned.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { get } from 'es-toolkit/compat'
|
||||
|
||||
/**
|
||||
* Machine-readable error code returned in a 403 response body by the cloud
|
||||
* backend (comfy-api at api.comfy.org and the cloud ingest server) when the
|
||||
* account has been banned. It distinguishes a ban from an ordinary
|
||||
* access-denied 403.
|
||||
*/
|
||||
const ACCOUNT_BANNED_CODE = 'ACCOUNT_BANNED'
|
||||
|
||||
export function isAccountBannedResponseBody(body: unknown): boolean {
|
||||
return get(body, 'code') === ACCOUNT_BANNED_CODE
|
||||
}
|
||||
|
||||
type AccountBannedListener = () => void
|
||||
|
||||
const listeners = new Set<AccountBannedListener>()
|
||||
|
||||
/**
|
||||
* Subscribe to account-ban detection. Returns an unsubscribe function.
|
||||
*
|
||||
* Detection is decoupled from handling so any client that talks to an
|
||||
* authenticated cloud surface (the local ComfyUI server on cloud, or the
|
||||
* registry at api.comfy.org on every distribution) can report a ban through one
|
||||
* channel, while the app decides how to surface it (route to a banned page on
|
||||
* cloud, toast on local).
|
||||
*/
|
||||
export function onAccountBanned(listener: AccountBannedListener): () => void {
|
||||
listeners.add(listener)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
export function notifyAccountBanned(): void {
|
||||
for (const listener of listeners) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
123
src/platform/auth/accountBannedInterceptors.test.ts
Normal file
123
src/platform/auth/accountBannedInterceptors.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import axios, { AxiosError, AxiosHeaders } from 'axios'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { onAccountBanned } from '@/platform/auth/accountBanned'
|
||||
import {
|
||||
addAccountBannedInterceptor,
|
||||
installAccountBannedFetchInterceptor
|
||||
} from '@/platform/auth/accountBannedInterceptors'
|
||||
|
||||
vi.mock('@/config/comfyApi', () => ({
|
||||
getComfyApiBaseUrl: () => 'https://api.comfy.org',
|
||||
getComfyPlatformBaseUrl: () => 'https://platform.comfy.org'
|
||||
}))
|
||||
|
||||
function bannedResponse(): Response {
|
||||
return new Response(JSON.stringify({ code: 'ACCOUNT_BANNED' }), {
|
||||
status: 403
|
||||
})
|
||||
}
|
||||
|
||||
describe('installAccountBannedFetchInterceptor', () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
it('notifies listeners when one of our cloud hosts returns a banned 403', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(bannedResponse())
|
||||
installAccountBannedFetchInterceptor()
|
||||
const onBanned = vi.fn()
|
||||
const unsubscribe = onAccountBanned(onBanned)
|
||||
|
||||
await fetch('https://api.comfy.org/customers/balance')
|
||||
await vi.waitFor(() => expect(onBanned).toHaveBeenCalledTimes(1))
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('ignores a banned 403 from a third-party host', async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(bannedResponse())
|
||||
installAccountBannedFetchInterceptor()
|
||||
const onBanned = vi.fn()
|
||||
const unsubscribe = onAccountBanned(onBanned)
|
||||
|
||||
await fetch('https://evil.example.com/whatever')
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(onBanned).not.toHaveBeenCalled()
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('does not notify for an ordinary 403 and returns the body intact', async () => {
|
||||
globalThis.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
new Response(JSON.stringify({ code: 'ACCESS_DENIED' }), { status: 403 })
|
||||
)
|
||||
installAccountBannedFetchInterceptor()
|
||||
const onBanned = vi.fn()
|
||||
const unsubscribe = onAccountBanned(onBanned)
|
||||
|
||||
const response = await fetch('https://api.comfy.org/customers/balance')
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(onBanned).not.toHaveBeenCalled()
|
||||
expect(await response.json()).toEqual({ code: 'ACCESS_DENIED' })
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addAccountBannedInterceptor', () => {
|
||||
let client: ReturnType<typeof axios.create>
|
||||
|
||||
beforeEach(() => {
|
||||
client = axios.create()
|
||||
addAccountBannedInterceptor(client)
|
||||
})
|
||||
|
||||
it('notifies listeners when a request rejects with a banned 403', async () => {
|
||||
client.interceptors.request.use(() =>
|
||||
Promise.reject(
|
||||
new AxiosError('banned', 'ERR_BAD_REQUEST', undefined, undefined, {
|
||||
status: 403,
|
||||
data: { code: 'ACCOUNT_BANNED' },
|
||||
statusText: 'Forbidden',
|
||||
headers: {},
|
||||
config: { headers: new AxiosHeaders() }
|
||||
})
|
||||
)
|
||||
)
|
||||
const onBanned = vi.fn()
|
||||
const unsubscribe = onAccountBanned(onBanned)
|
||||
|
||||
await expect(client.get('/whatever')).rejects.toThrow()
|
||||
expect(onBanned).toHaveBeenCalledTimes(1)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('does not notify for an ordinary 403 rejection', async () => {
|
||||
client.interceptors.request.use(() =>
|
||||
Promise.reject(
|
||||
new AxiosError('denied', 'ERR_BAD_REQUEST', undefined, undefined, {
|
||||
status: 403,
|
||||
data: { code: 'ACCESS_DENIED' },
|
||||
statusText: 'Forbidden',
|
||||
headers: {},
|
||||
config: { headers: new AxiosHeaders() }
|
||||
})
|
||||
)
|
||||
)
|
||||
const onBanned = vi.fn()
|
||||
const unsubscribe = onAccountBanned(onBanned)
|
||||
|
||||
await expect(client.get('/whatever')).rejects.toThrow()
|
||||
expect(onBanned).not.toHaveBeenCalled()
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
})
|
||||
81
src/platform/auth/accountBannedInterceptors.ts
Normal file
81
src/platform/auth/accountBannedInterceptors.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import axios from 'axios'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
|
||||
import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
||||
import {
|
||||
isAccountBannedResponseBody,
|
||||
notifyAccountBanned
|
||||
} from '@/platform/auth/accountBanned'
|
||||
|
||||
/**
|
||||
* Registers a response interceptor that reports an account ban whenever the
|
||||
* client receives a 403 carrying the ACCOUNT_BANNED code, then rethrows so each
|
||||
* caller's existing error handling is unaffected.
|
||||
*/
|
||||
export function addAccountBannedInterceptor(client: AxiosInstance): void {
|
||||
client.interceptors.response.use(undefined, (error: unknown) => {
|
||||
if (
|
||||
axios.isAxiosError(error) &&
|
||||
error.response?.status === 403 &&
|
||||
isAccountBannedResponseBody(error.response.data)
|
||||
) {
|
||||
notifyAccountBanned()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the global fetch so a banned 403 from a fetch-based call to one of our
|
||||
* cloud hosts (the comfy-api registry, the cloud ingest server, the platform,
|
||||
* subscriptions, etc.) reports an account ban. Responses from third-party hosts
|
||||
* are ignored, and the original response is returned untouched to callers.
|
||||
*/
|
||||
export function installAccountBannedFetchInterceptor(): void {
|
||||
const originalFetch = globalThis.fetch.bind(globalThis)
|
||||
globalThis.fetch = async (
|
||||
...args: Parameters<typeof fetch>
|
||||
): Promise<Response> => {
|
||||
const response = await originalFetch(...args)
|
||||
if (response.status === 403 && isOurCloudUrl(requestUrl(args[0]))) {
|
||||
void reportIfBanned(response.clone())
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
function requestUrl(input: Parameters<typeof fetch>[0]): string {
|
||||
if (typeof input === 'string') return input
|
||||
if (input instanceof URL) return input.href
|
||||
return input.url
|
||||
}
|
||||
|
||||
function isOurCloudUrl(url: string): boolean {
|
||||
try {
|
||||
const host = new URL(url, globalThis.location?.href).host
|
||||
return ourCloudHosts().has(host)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function ourCloudHosts(): Set<string> {
|
||||
const hosts = new Set<string>()
|
||||
hosts.add(new URL(getComfyApiBaseUrl()).host)
|
||||
hosts.add(new URL(getComfyPlatformBaseUrl()).host)
|
||||
if (globalThis.location?.host) {
|
||||
hosts.add(globalThis.location.host)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
async function reportIfBanned(response: Response): Promise<void> {
|
||||
try {
|
||||
const body: unknown = await response.json()
|
||||
if (isAccountBannedResponseBody(body)) {
|
||||
notifyAccountBanned()
|
||||
}
|
||||
} catch {
|
||||
// Body was not banned-shaped JSON; treat as an ordinary 403.
|
||||
}
|
||||
}
|
||||
38
src/platform/cloud/onboarding/CloudBannedView.vue
Normal file
38
src/platform/cloud/onboarding/CloudBannedView.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="flex h-full items-center justify-center p-6">
|
||||
<div class="max-w-[100vw] text-center lg:w-[500px]">
|
||||
<h2 class="mb-3 text-xl text-text-primary">
|
||||
{{ $t('cloudOnboarding.banned.title') }}
|
||||
</h2>
|
||||
<p class="mb-5 text-muted">
|
||||
{{ $t('cloudOnboarding.banned.message') }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<Button as="a" :href="supportUrl" target="_blank" rel="noopener">
|
||||
{{ $t('cloudOnboarding.banned.contactSupport') }}
|
||||
</Button>
|
||||
<Button variant="textonly" @click="handleSignOut">
|
||||
{{ $t('cloudOnboarding.banned.signOut') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
|
||||
const supportUrl = 'https://support.comfy.org'
|
||||
|
||||
const router = useRouter()
|
||||
const { logout } = useAuthActions()
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await logout()
|
||||
await router.replace({ name: 'cloud-login' })
|
||||
}
|
||||
</script>
|
||||
@@ -114,6 +114,12 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudSorryContactSupportView.vue')
|
||||
},
|
||||
{
|
||||
path: 'banned',
|
||||
name: 'cloud-banned',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudBannedView.vue')
|
||||
},
|
||||
{
|
||||
path: 'auth-timeout',
|
||||
name: 'cloud-auth-timeout',
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useReleaseService } from '@/platform/updates/common/releaseService'
|
||||
|
||||
// Hoist the mock to avoid hoisting issues
|
||||
const mockAxiosInstance = vi.hoisted(() => ({
|
||||
get: vi.fn()
|
||||
get: vi.fn(),
|
||||
interceptors: { response: { use: vi.fn() } }
|
||||
}))
|
||||
|
||||
vi.mock('axios', () => ({
|
||||
|
||||
@@ -3,6 +3,7 @@ import axios from 'axios'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { addAccountBannedInterceptor } from '@/platform/auth/accountBannedInterceptors'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
@@ -20,6 +21,8 @@ const releaseApiClient = axios.create({
|
||||
}
|
||||
})
|
||||
|
||||
addAccountBannedInterceptor(releaseApiClient)
|
||||
|
||||
// Release service for fetching release notes
|
||||
export const useReleaseService = () => {
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -9,7 +9,8 @@ const {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn()
|
||||
delete: vi.fn(),
|
||||
interceptors: { response: { use: vi.fn() } }
|
||||
},
|
||||
mockGetAuthHeaderOrThrow: vi.fn(),
|
||||
mockGetFirebaseAuthHeaderOrThrow: vi.fn()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { addAccountBannedInterceptor } from '@/platform/auth/accountBannedInterceptors'
|
||||
import type { SubscriptionTier } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type {
|
||||
WorkspaceId,
|
||||
@@ -287,6 +288,8 @@ const workspaceApiClient = axios.create({
|
||||
}
|
||||
})
|
||||
|
||||
addAccountBannedInterceptor(workspaceApiClient)
|
||||
|
||||
async function getAuthHeaderOrThrow() {
|
||||
return useAuthStore().getAuthHeaderOrThrow()
|
||||
}
|
||||
|
||||
@@ -134,14 +134,16 @@ if (isCloud) {
|
||||
'cloud-signup',
|
||||
'cloud-forgot-password',
|
||||
'cloud-oauth-consent',
|
||||
'cloud-sorry-contact-support'
|
||||
'cloud-sorry-contact-support',
|
||||
'cloud-banned'
|
||||
])
|
||||
const PUBLIC_ROUTE_PATHS = new Set([
|
||||
'/cloud/login',
|
||||
'/cloud/signup',
|
||||
'/cloud/forgot-password',
|
||||
'/cloud/oauth/consent',
|
||||
'/cloud/sorry-contact-support'
|
||||
'/cloud/sorry-contact-support',
|
||||
'/cloud/banned'
|
||||
])
|
||||
|
||||
function isPublicRoute(to: RouteLocationNormalized) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AxiosError, AxiosResponse } from 'axios'
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { addAccountBannedInterceptor } from '@/platform/auth/accountBannedInterceptors'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
@@ -18,6 +19,8 @@ const registryApiClient = axios.create({
|
||||
}
|
||||
})
|
||||
|
||||
addAccountBannedInterceptor(registryApiClient)
|
||||
|
||||
/**
|
||||
* Service for interacting with the Comfy Registry API
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
|
||||
// Hoist the mocks to avoid hoisting issues
|
||||
const mockAxiosInstance = vi.hoisted(() => ({
|
||||
get: vi.fn()
|
||||
get: vi.fn(),
|
||||
interceptors: { response: { use: vi.fn() } }
|
||||
}))
|
||||
|
||||
const mockAuthStore = vi.hoisted(() => ({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ref, watch } from 'vue'
|
||||
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { d } from '@/i18n'
|
||||
import { addAccountBannedInterceptor } from '@/platform/auth/accountBannedInterceptors'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
@@ -30,6 +31,8 @@ const customerApiClient = axios.create({
|
||||
}
|
||||
})
|
||||
|
||||
addAccountBannedInterceptor(customerApiClient)
|
||||
|
||||
export const useCustomerEventsService = () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
@@ -3,6 +3,7 @@ import axios from 'axios'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { addAccountBannedInterceptor } from '@/platform/auth/accountBannedInterceptors'
|
||||
import { api } from '@/scripts/api'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
@@ -45,6 +46,8 @@ const managerApiClient = axios.create({
|
||||
}
|
||||
})
|
||||
|
||||
addAccountBannedInterceptor(managerApiClient)
|
||||
|
||||
/**
|
||||
* Service for interacting with the ComfyUI Manager API
|
||||
* Provides methods for managing packs, ComfyUI-Manager queue operations, and system functions
|
||||
|
||||
Reference in New Issue
Block a user