mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
50 Commits
fix/codera
...
manager-mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fdbc28dac | ||
|
|
8075db41fa | ||
|
|
4f9470751d | ||
|
|
f1610db470 | ||
|
|
1aee43425a | ||
|
|
bdd1230902 | ||
|
|
b01056e31f | ||
|
|
e780e1d6d6 | ||
|
|
f8953a874d | ||
|
|
aba2e5edfc | ||
|
|
eb5c49f67b | ||
|
|
54a0981031 | ||
|
|
de6ed34836 | ||
|
|
588bd99c3a | ||
|
|
c28d4514fc | ||
|
|
730b278fa0 | ||
|
|
2e2647d1d0 | ||
|
|
7073c9d57f | ||
|
|
1cb94ca677 | ||
|
|
558cfcc527 | ||
|
|
babe324f8b | ||
|
|
a83a7faea4 | ||
|
|
dc8589672c | ||
|
|
0f7e3f7100 | ||
|
|
8fbd076b9c | ||
|
|
096e0c9ad0 | ||
|
|
60e2f14516 | ||
|
|
0f9d1f833d | ||
|
|
4249fe7a76 | ||
|
|
5e4371507f | ||
|
|
39ad01826a | ||
|
|
b683a81239 | ||
|
|
ba1d067c04 | ||
|
|
f241b7f7a0 | ||
|
|
b3486695d3 | ||
|
|
7725b87c80 | ||
|
|
ced03ceee8 | ||
|
|
4a67a83252 | ||
|
|
3862d78fb3 | ||
|
|
05ec65f636 | ||
|
|
dc2a4708f3 | ||
|
|
eee3b74547 | ||
|
|
3c8adf4f80 | ||
|
|
d3a629ce89 | ||
|
|
7c14b26321 | ||
|
|
5867103981 | ||
|
|
363a46b1bf | ||
|
|
1f41367454 | ||
|
|
2aed31ade1 | ||
|
|
366c55070c |
130
src/components/common/DotSpinner.vue
Normal file
130
src/components/common/DotSpinner.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center justify-center"
|
||||
:style="{ width: size + 'px', height: size + 'px' }"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
class="animate-spin"
|
||||
:style="{ animationDuration: duration }"
|
||||
>
|
||||
<g clip-path="url(#clip0_776_9582)">
|
||||
<!-- Top dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7 2.21053C7.61042 2.21053 8.10526 1.71568 8.10526 1.10526C8.10526 0.494843 7.61042 0 7 0C6.38958 0 5.89474 0.494843 5.89474 1.10526C5.89474 1.71568 6.38958 2.21053 7 2.21053Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.25s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.21053 7C2.21053 7.61042 1.71568 8.10526 1.10526 8.10526C0.494843 8.10526 0 7.61042 0 7C0 6.38958 0.494843 5.89474 1.10526 5.89474C1.71568 5.89474 2.21053 6.38958 2.21053 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.5s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 7C14 7.61042 13.5052 8.10526 12.8947 8.10526C12.2843 8.10526 11.7895 7.61042 11.7895 7C11.7895 6.38958 12.2843 5.89474 12.8947 5.89474C13.5052 5.89474 14 6.38958 14 7Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.75s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.10526 12.8947C8.10526 13.5052 7.61041 14 6.99999 14C6.38957 14 5.89473 13.5052 5.89473 12.8947C5.89473 12.2843 6.38957 11.7895 6.99999 11.7895C7.61041 11.7895 8.10526 12.2843 8.10526 12.8947Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.125s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 3.61349C2.48203 4.04513 3.18184 4.04513 3.61347 3.61349C4.0451 3.18186 4.0451 2.48205 3.61347 2.05042C3.18184 1.61878 2.48203 1.61878 2.05039 2.05042C1.61876 2.48205 1.61876 3.18186 2.05039 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.625s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 11.9496C11.518 12.3812 10.8182 12.3812 10.3865 11.9496C9.9549 11.5179 9.9549 10.8181 10.3865 10.3865C10.8182 9.95485 11.518 9.95485 11.9496 10.3865C12.3812 10.8181 12.3812 11.5179 11.9496 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Bottom-left dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.875s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.05039 11.9496C2.48203 12.3812 3.18184 12.3812 3.61347 11.9496C4.0451 11.5179 4.0451 10.8181 3.61347 10.3865C3.18184 9.95485 2.48203 9.95485 2.05039 10.3865C1.61876 10.8181 1.61876 11.5179 2.05039 11.9496Z"
|
||||
:fill="color"
|
||||
/>
|
||||
<!-- Top-right dot -->
|
||||
<path
|
||||
class="dot-animation"
|
||||
style="animation-delay: 0.375s"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.9496 3.61349C11.518 4.04513 10.8182 4.04513 10.3865 3.61349C9.9549 3.18186 9.9549 2.48205 10.3865 2.05042C10.8182 1.61878 11.518 1.61878 11.9496 2.05042C12.3812 2.48205 12.3812 3.18186 11.9496 3.61349Z"
|
||||
:fill="color"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_776_9582">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const { size = 24, duration = '2s' } = defineProps<{
|
||||
size?: number
|
||||
duration?: string
|
||||
}>()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const color = computed(() =>
|
||||
colorPaletteStore.completedActivePalette.light_theme ? '#2C2B30' : '#D4D4D4'
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dot-animation {
|
||||
animation: dot-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dot-pulse {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</ListBox>
|
||||
<div v-if="isManagerInstalled" class="flex justify-end py-3">
|
||||
<div v-if="!isLegacyManager" class="flex justify-end py-3">
|
||||
<PackInstallButton
|
||||
:disabled="isLoading || !!error || missingNodePacks.length === 0"
|
||||
:node-packs="missingNodePacks"
|
||||
@@ -45,14 +45,12 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -60,22 +58,11 @@ const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error, missingCoreNodes } =
|
||||
useMissingNodes()
|
||||
|
||||
// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
|
||||
// This allows us to conditionally show the Manager button only when the extension is available
|
||||
// TODO: Remove this check when Manager functionality is fully migrated into core
|
||||
const isManagerInstalled = computed(() => {
|
||||
return aboutPanelStore.badges.some(
|
||||
(badge) =>
|
||||
badge.label.includes('ComfyUI-Manager') ||
|
||||
badge.url.includes('ComfyUI-Manager')
|
||||
)
|
||||
})
|
||||
const isLegacyManager = ref(false)
|
||||
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
@@ -103,6 +90,13 @@ const openManager = () => {
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const isLegacyResponse = await useComfyManagerService().isLegacyManagerUI()
|
||||
if (isLegacyResponse?.is_legacy_manager_ui) {
|
||||
isLegacyManager.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -30,11 +30,20 @@ const defaultMockTaskLogs = [
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn(() => ({
|
||||
taskLogs: [...defaultMockTaskLogs]
|
||||
taskLogs: [...defaultMockTaskLogs],
|
||||
succeededTasksLogs: [...defaultMockTaskLogs],
|
||||
failedTasksLogs: [...defaultMockTaskLogs],
|
||||
managerQueue: { historyCount: 2 },
|
||||
isLoading: false
|
||||
})),
|
||||
useManagerProgressDialogStore: vi.fn(() => ({
|
||||
isExpanded: true,
|
||||
collapse: mockCollapse
|
||||
activeTabIndex: 0,
|
||||
getActiveTabIndex: vi.fn(() => 0),
|
||||
setActiveTabIndex: vi.fn(),
|
||||
toggle: vi.fn(),
|
||||
collapse: mockCollapse,
|
||||
expand: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
'max-h-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div v-for="(panel, index) in taskPanels" :key="index">
|
||||
<div v-for="(log, index) in focusedLogs" :key="index">
|
||||
<Panel
|
||||
:expanded="collapsedPanels[index] || false"
|
||||
toggleable
|
||||
@@ -27,7 +27,7 @@
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full py-2">
|
||||
<div class="flex flex-col text-sm font-medium leading-normal">
|
||||
<span>{{ panel.taskName }}</span>
|
||||
<span>{{ log.taskName }}</span>
|
||||
<span class="text-muted">
|
||||
{{
|
||||
isInProgress(index)
|
||||
@@ -52,24 +52,24 @@
|
||||
</template>
|
||||
<div
|
||||
:ref="
|
||||
index === taskPanels.length - 1
|
||||
index === focusedLogs.length - 1
|
||||
? (el) => (lastPanelRef = el as HTMLElement)
|
||||
: undefined
|
||||
"
|
||||
class="overflow-y-auto h-64 rounded-lg bg-black"
|
||||
:class="{
|
||||
'h-64': index !== taskPanels.length - 1,
|
||||
'flex-grow': index === taskPanels.length - 1
|
||||
'h-64': index !== focusedLogs.length - 1,
|
||||
'flex-grow': index === focusedLogs.length - 1
|
||||
}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-for="(log, logIndex) in panel.logs"
|
||||
v-for="(logLine, logIndex) in log.logs"
|
||||
:key="logIndex"
|
||||
class="text-neutral-400 dark-theme:text-muted"
|
||||
>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ logLine }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,17 +90,23 @@ import {
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const { taskLogs } = useComfyManagerStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const isInProgress = (index: number) =>
|
||||
index === taskPanels.value.length - 1 && managerStore.uncompletedCount > 0
|
||||
index === comfyManagerStore.managerQueue.historyCount - 1 &&
|
||||
comfyManagerStore.isLoading
|
||||
|
||||
const taskPanels = computed(() => taskLogs)
|
||||
const isExpanded = computed(() => progressDialogContent.isExpanded)
|
||||
const isCollapsed = computed(() => !isExpanded.value)
|
||||
|
||||
const focusedLogs = computed(() => {
|
||||
if (progressDialogContent.getActiveTabIndex() === 0) {
|
||||
return comfyManagerStore.succeededTasksLogs
|
||||
}
|
||||
return comfyManagerStore.failedTasksLogs
|
||||
})
|
||||
|
||||
const collapsedPanels = ref<Record<number, boolean>>({})
|
||||
const togglePanel = (index: number) => {
|
||||
collapsedPanels.value[index] = !collapsedPanels.value[index]
|
||||
@@ -115,7 +121,7 @@ const { y: scrollY } = useScroll(sectionsContainerRef, {
|
||||
|
||||
const lastPanelRef = ref<HTMLElement | null>(null)
|
||||
const isUserScrolling = ref(false)
|
||||
const lastPanelLogs = computed(() => taskPanels.value?.at(-1)?.logs)
|
||||
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
|
||||
|
||||
const isAtBottom = (el: HTMLElement | null) => {
|
||||
if (!el) return false
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
:suggestions="suggestions"
|
||||
:is-missing-tab="isMissingTab"
|
||||
:sort-options="sortOptions"
|
||||
:is-update-available-tab="isUpdateAvailableTab"
|
||||
/>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
<h2 class="text-lg font-normal text-left">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
<Tag
|
||||
v-tooltip.left="$t('manager.legacyManagerUIDescription')"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
:value="$t('manager.legacyManagerUI')"
|
||||
class="cursor-help"
|
||||
:pt="{
|
||||
root: { class: 'text-xs' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
import PackVersionBadge from './PackVersionBadge.vue'
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
@@ -120,7 +119,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
})
|
||||
|
||||
it('falls back to NIGHTLY when nodePack.id is missing', () => {
|
||||
@@ -134,7 +133,7 @@ describe('PackVersionBadge', () => {
|
||||
|
||||
const badge = wrapper.find('[role="button"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.find('span').text()).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(badge.find('span').text()).toBe('nightly')
|
||||
})
|
||||
|
||||
it('toggles the popover when button is clicked', async () => {
|
||||
|
||||
@@ -42,8 +42,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
|
||||
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const TRUNCATED_HASH_LENGTH = 7
|
||||
@@ -64,11 +63,11 @@ const popoverRef = ref()
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const installedVersion = computed(() => {
|
||||
if (!nodePack.id) return SelectedVersion.NIGHTLY
|
||||
if (!nodePack.id) return 'nightly'
|
||||
const version =
|
||||
managerStore.installedPacks[nodePack.id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
'nightly'
|
||||
|
||||
// If Git hash, truncate to 7 characters
|
||||
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH)
|
||||
|
||||
@@ -8,7 +8,8 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import { SelectedVersion } from '@/types/comfyManagerTypes'
|
||||
|
||||
// SelectedVersion is now using direct strings instead of enum
|
||||
|
||||
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
|
||||
|
||||
@@ -123,8 +124,8 @@ describe('PackVersionSelectorPopover', () => {
|
||||
expect(options.length).toBe(defaultMockVersions.length + 2) // 2 special options + version options
|
||||
|
||||
// Check that special options exist
|
||||
expect(options.some((o) => o.value === SelectedVersion.NIGHTLY)).toBe(true)
|
||||
expect(options.some((o) => o.value === SelectedVersion.LATEST)).toBe(true)
|
||||
expect(options.some((o) => o.value === 'nightly')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'latest')).toBe(true)
|
||||
|
||||
// Check that version options exist
|
||||
expect(options.some((o) => o.value === '1.0.0')).toBe(true)
|
||||
@@ -304,7 +305,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
|
||||
it('defaults to nightly when publisher name is "Unclaimed"', async () => {
|
||||
@@ -325,7 +326,7 @@ describe('PackVersionSelectorPopover', () => {
|
||||
await waitForPromises()
|
||||
const listbox = wrapper.findComponent(Listbox)
|
||||
expect(listbox.exists()).toBe(true)
|
||||
expect(listbox.props('modelValue')).toBe(SelectedVersion.NIGHTLY)
|
||||
expect(listbox.props('modelValue')).toBe('nightly')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,12 +69,8 @@ import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
import { isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
@@ -92,19 +88,20 @@ const managerStore = useComfyManagerStore()
|
||||
|
||||
const isQueueing = ref(false)
|
||||
|
||||
const selectedVersion = ref<string>(SelectedVersion.LATEST)
|
||||
const selectedVersion = ref<string>('latest')
|
||||
onMounted(() => {
|
||||
const initialVersion = getInitialSelectedVersion() ?? SelectedVersion.LATEST
|
||||
const initialVersion = getInitialSelectedVersion() ?? 'latest'
|
||||
selectedVersion.value =
|
||||
// Use NIGHTLY when version is a Git hash
|
||||
isSemVer(initialVersion) ? initialVersion : SelectedVersion.NIGHTLY
|
||||
isSemVer(initialVersion) ? initialVersion : 'nightly'
|
||||
})
|
||||
|
||||
const getInitialSelectedVersion = () => {
|
||||
if (!nodePack.id) return
|
||||
|
||||
// If unclaimed, set selected version to nightly
|
||||
if (nodePack.publisher?.name === 'Unclaimed') return SelectedVersion.NIGHTLY
|
||||
if (nodePack.publisher?.name === 'Unclaimed')
|
||||
return 'nightly' as ManagerComponents['schemas']['SelectedVersion']
|
||||
|
||||
// If node pack is installed, set selected version to the installed version
|
||||
if (managerStore.isPackInstalled(nodePack.id))
|
||||
@@ -143,7 +140,7 @@ const onNodePackChange = async () => {
|
||||
// Add Latest option
|
||||
const defaultVersions = [
|
||||
{
|
||||
value: SelectedVersion.LATEST,
|
||||
value: 'latest' as ManagerComponents['schemas']['SelectedVersion'],
|
||||
label: t('manager.latestVersion')
|
||||
}
|
||||
]
|
||||
@@ -151,7 +148,7 @@ const onNodePackChange = async () => {
|
||||
// Add Nightly option if there is a non-empty `repository` field
|
||||
if (nodePack.repository?.length) {
|
||||
defaultVersions.push({
|
||||
value: SelectedVersion.NIGHTLY,
|
||||
value: 'nightly' as ManagerComponents['schemas']['SelectedVersion'],
|
||||
label: t('manager.nightlyVersion')
|
||||
})
|
||||
}
|
||||
@@ -172,12 +169,16 @@ whenever(
|
||||
|
||||
const handleSubmit = async () => {
|
||||
isQueueing.value = true
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
await managerStore.installPack.call({
|
||||
id: nodePack.id,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
version: selectedVersion.value,
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
selected_version: selectedVersion.value
|
||||
})
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import Button from 'primevue/button'
|
||||
|
||||
const {
|
||||
label,
|
||||
loading = false,
|
||||
loadingMessage,
|
||||
fullWidth = false,
|
||||
variant = 'default'
|
||||
|
||||
@@ -15,12 +15,8 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
InstallPackParams,
|
||||
ManagerChannel,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
const TOGGLE_DEBOUNCE_MS = 256
|
||||
|
||||
@@ -28,37 +24,42 @@ const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackEnabled, enablePack, disablePack, installedPacks } =
|
||||
useComfyManagerStore()
|
||||
const { isPackEnabled, enablePack, disablePack } = useComfyManagerStore()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const isEnabled = computed(() => isPackEnabled(nodePack.id))
|
||||
const version = computed(() => {
|
||||
const id = nodePack.id
|
||||
if (!id) return SelectedVersion.NIGHTLY
|
||||
return (
|
||||
installedPacks[id]?.ver ??
|
||||
nodePack.latest_version?.version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
)
|
||||
})
|
||||
|
||||
const handleEnable = () =>
|
||||
enablePack.call({
|
||||
const handleEnable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for enabling')
|
||||
}
|
||||
return enablePack.call({
|
||||
id: nodePack.id,
|
||||
version: version.value,
|
||||
selected_version: version.value,
|
||||
version:
|
||||
nodePack.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']),
|
||||
selected_version:
|
||||
nodePack.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion']),
|
||||
repository: nodePack.repository ?? '',
|
||||
channel: ManagerChannel.DEFAULT,
|
||||
mode: 'default' as InstallPackParams['mode']
|
||||
channel: 'default' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
skip_post_install: false
|
||||
})
|
||||
}
|
||||
|
||||
const handleDisable = () =>
|
||||
disablePack({
|
||||
const handleDisable = () => {
|
||||
if (!nodePack.id) {
|
||||
throw new Error('Node ID is required for disabling')
|
||||
}
|
||||
return disablePack({
|
||||
id: nodePack.id,
|
||||
version: version.value
|
||||
version:
|
||||
nodePack.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggle = async (enable: boolean) => {
|
||||
if (isLoading.value) return
|
||||
@@ -67,10 +68,16 @@ const handleToggle = async (enable: boolean) => {
|
||||
if (enable) {
|
||||
await handleEnable()
|
||||
} else {
|
||||
handleDisable()
|
||||
await handleDisable()
|
||||
}
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const onToggle = debounce(handleToggle, TOGGLE_DEBOUNCE_MS, { trailing: true })
|
||||
const onToggle = debounce(
|
||||
(enable: boolean) => {
|
||||
void handleToggle(enable)
|
||||
},
|
||||
TOGGLE_DEBOUNCE_MS,
|
||||
{ trailing: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -19,13 +19,9 @@ import { inject, ref } from 'vue'
|
||||
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
IsInstallingKey,
|
||||
ManagerChannel,
|
||||
ManagerDatabaseSource,
|
||||
SelectedVersion
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { IsInstallingKey } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
@@ -43,19 +39,26 @@ const onClick = (): void => {
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (installItem: NodePack) => {
|
||||
const createPayload = (
|
||||
installItem: NodePack
|
||||
): ManagerComponents['schemas']['InstallPackParams'] => {
|
||||
if (!installItem.id) {
|
||||
throw new Error('Node ID is required for installation')
|
||||
}
|
||||
|
||||
const isUnclaimedPack = installItem.publisher?.name === 'Unclaimed'
|
||||
const versionToInstall = isUnclaimedPack
|
||||
? SelectedVersion.NIGHTLY
|
||||
: installItem.latest_version?.version ?? SelectedVersion.LATEST
|
||||
? ('nightly' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
: installItem.latest_version?.version ??
|
||||
('latest' as ManagerComponents['schemas']['SelectedVersion'])
|
||||
|
||||
return {
|
||||
id: installItem.id,
|
||||
version: versionToInstall,
|
||||
repository: installItem.repository ?? '',
|
||||
channel: ManagerChannel.DEV,
|
||||
mode: ManagerDatabaseSource.CACHE,
|
||||
selected_version: versionToInstall,
|
||||
version: versionToInstall
|
||||
channel: 'dev' as ManagerComponents['schemas']['ManagerChannel'],
|
||||
mode: 'cache' as ManagerComponents['schemas']['ManagerDatabaseSource'],
|
||||
selected_version: versionToInstall
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +68,7 @@ const installPack = (item: NodePack) =>
|
||||
const installAllPacks = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
|
||||
isInstalling.value = true
|
||||
// isInstalling.value = true
|
||||
|
||||
const uninstalledPacks = nodePacks.filter(
|
||||
(pack) => !managerStore.isPackInstalled(pack.id)
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<script setup lang="ts">
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { ManagerPackInfo } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
@@ -26,16 +25,16 @@ const { nodePacks } = defineProps<{
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (uninstallItem: NodePack): ManagerPackInfo => {
|
||||
return {
|
||||
id: uninstallItem.id,
|
||||
version: uninstallItem.latest_version?.version
|
||||
const uninstallPack = (item: NodePack) => {
|
||||
if (!item.id) {
|
||||
throw new Error('Node ID is required for uninstallation')
|
||||
}
|
||||
return managerStore.uninstallPack({
|
||||
id: item.id,
|
||||
version: item.latest_version?.version ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
const uninstallPack = (item: NodePack) =>
|
||||
managerStore.uninstallPack(createPayload(item))
|
||||
|
||||
const uninstallItems = async () => {
|
||||
if (!nodePacks?.length) return
|
||||
await Promise.all(nodePacks.map(uninstallPack))
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<PackActionButton
|
||||
v-bind="$attrs"
|
||||
variant="black"
|
||||
:label="$t('manager.updateAll')"
|
||||
:loading="isUpdating"
|
||||
:loading-message="$t('g.updating')"
|
||||
@action="updateAllPacks"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: NodePack[]
|
||||
}>()
|
||||
|
||||
const isUpdating = ref<boolean>(false)
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
const createPayload = (updateItem: NodePack) => {
|
||||
return {
|
||||
id: updateItem.id!,
|
||||
version: updateItem.latest_version!.version!
|
||||
}
|
||||
}
|
||||
|
||||
const updatePack = async (item: NodePack) => {
|
||||
if (!item.id || !item.latest_version?.version) {
|
||||
console.warn('Pack missing required id or version:', item)
|
||||
return
|
||||
}
|
||||
await managerStore.updatePack.call(createPayload(item))
|
||||
}
|
||||
|
||||
const updateAllPacks = async () => {
|
||||
if (!nodePacks?.length) {
|
||||
console.warn('No packs provided for update')
|
||||
return
|
||||
}
|
||||
|
||||
isUpdating.value = true
|
||||
|
||||
const updatablePacks = nodePacks.filter((pack) =>
|
||||
managerStore.isPackInstalled(pack.id)
|
||||
)
|
||||
|
||||
if (!updatablePacks.length) {
|
||||
console.info('No installed packs available for update')
|
||||
isUpdating.value = false
|
||||
return
|
||||
}
|
||||
|
||||
console.info(`Starting update of ${updatablePacks.length} packs`)
|
||||
|
||||
try {
|
||||
await Promise.all(updatablePacks.map(updatePack))
|
||||
managerStore.updatePack.clear()
|
||||
console.info('All packs updated successfully')
|
||||
} catch (error) {
|
||||
console.error('Pack update failed:', error)
|
||||
console.error(
|
||||
'Failed packs info:',
|
||||
updatablePacks.map((p) => p.id)
|
||||
)
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -119,12 +119,20 @@ provide(IsInstallingKey, isInstalling)
|
||||
|
||||
const { isPackInstalled, isPackEnabled } = useComfyManagerStore()
|
||||
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isDisabled = computed(
|
||||
() => isInstalled.value && !isPackEnabled(nodePack?.id)
|
||||
)
|
||||
const isDisabled = ref(false)
|
||||
const managerStore = useComfyManagerStore()
|
||||
|
||||
whenever(isInstalled, () => (isInstalling.value = false))
|
||||
// Watch the installedPacks object directly (which gets updated from WebSocket)
|
||||
whenever(
|
||||
() => managerStore.installedPacksIds,
|
||||
() => {
|
||||
const isInstalled = isPackInstalled(nodePack?.id)
|
||||
isDisabled.value = isInstalled && !isPackEnabled(nodePack?.id)
|
||||
|
||||
// Update isInstalling state after installation is complete
|
||||
if (isInstalling.value && isInstalled) isInstalling.value = false
|
||||
}
|
||||
)
|
||||
|
||||
const nodesCount = computed(() =>
|
||||
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
:node-packs="missingNodePacks"
|
||||
:label="$t('manager.installAllMissingNodes')"
|
||||
/>
|
||||
<PackUpdateButton
|
||||
v-if="isUpdateAvailableTab && hasUpdateAvailable"
|
||||
:node-packs="updateAvailableNodePacks"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-3 text-sm">
|
||||
<div class="flex gap-6 ml-1">
|
||||
@@ -65,8 +69,10 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackUpdateButton from '@/components/dialog/content/manager/button/PackUpdateButton.vue'
|
||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
|
||||
import {
|
||||
type SearchOption,
|
||||
SortableAlgoliaField
|
||||
@@ -83,6 +89,7 @@ const { searchResults, sortOptions } = defineProps<{
|
||||
suggestions?: QuerySuggestion[]
|
||||
sortOptions?: SortableField[]
|
||||
isMissingTab?: boolean
|
||||
isUpdateAvailableTab?: boolean
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
@@ -96,6 +103,10 @@ const { t } = useI18n()
|
||||
// Get missing node packs from workflow with loading and error states
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
|
||||
// Use the composable to get update available nodes
|
||||
const { hasUpdateAvailable, updateAvailableNodePacks } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
const hasResults = computed(
|
||||
() => searchQuery.value?.trim() && searchResults?.length
|
||||
)
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
|
||||
class="w-full px-6 py-2 shadow-lg flex items-center justify-between"
|
||||
:class="{
|
||||
'rounded-t-none': progressDialogContent.isExpanded,
|
||||
'rounded-lg': !progressDialogContent.isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="justify-center text-sm font-bold leading-none">
|
||||
<div class="flex items-center text-base leading-none">
|
||||
<div class="flex items-center">
|
||||
<template v-if="isInProgress">
|
||||
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
|
||||
<DotSpinner duration="1s" class="mr-2" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else-if="isRestartCompleted">
|
||||
<span class="mr-2">🎉</span>
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="pi pi-check-circle mr-2 text-green-500" />
|
||||
<span class="leading-none">{{
|
||||
$t('manager.restartToApplyChanges')
|
||||
}}</span>
|
||||
<span class="mr-2">✅</span>
|
||||
<span>{{ $t('manager.restartToApplyChanges') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
|
||||
{{ comfyManagerStore.uncompletedCount }} {{ $t('g.progressCountOf') }}
|
||||
<span v-if="isInProgress" class="text-sm text-neutral-700">
|
||||
{{ completedTasksCount }} {{ $t('g.progressCountOf') }}
|
||||
{{ comfyManagerStore.taskLogs.length }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="!isInProgress"
|
||||
v-if="!isInProgress && !isRestartCompleted"
|
||||
rounded
|
||||
outlined
|
||||
class="px-4 py-2 rounded-md mr-4"
|
||||
class="mr-4 rounded-md border-2 px-3 text-neutral-600 border-neutral-900 hover:bg-neutral-100 !dark-theme:bg-transparent dark-theme:text-white dark-theme:border-white dark-theme:hover:bg-neutral-800"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('g.restart') }}
|
||||
{{ $t('manager.applyChanges') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="!isRestartCompleted"
|
||||
:icon="
|
||||
progressDialogContent.isExpanded
|
||||
? 'pi pi-chevron-up'
|
||||
@@ -44,6 +47,7 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
|
||||
@click.stop="progressDialogContent.toggle"
|
||||
@@ -53,6 +57,7 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="font-bold"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
@@ -65,9 +70,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
@@ -77,41 +83,93 @@ import {
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
// State management for restart process
|
||||
const isRestarting = ref<boolean>(false)
|
||||
const isRestartCompleted = ref<boolean>(false)
|
||||
|
||||
const isInProgress = computed(
|
||||
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
||||
)
|
||||
|
||||
const completedTasksCount = computed(() => {
|
||||
return (
|
||||
comfyManagerStore.succeededTasksIds.length +
|
||||
comfyManagerStore.failedTasksIds.length
|
||||
)
|
||||
})
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
}
|
||||
|
||||
const fallbackTaskName = t('g.installing')
|
||||
const fallbackTaskName = t('manager.installingDependencies')
|
||||
const currentTaskName = computed(() => {
|
||||
if (isRestarting.value) {
|
||||
return t('manager.restartingBackend')
|
||||
}
|
||||
if (isRestartCompleted.value) {
|
||||
return t('manager.extensionsSuccessfullyInstalled')
|
||||
}
|
||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
||||
const task = comfyManagerStore.taskLogs.at(-1)
|
||||
return task?.taskName ?? fallbackTaskName
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
const onReconnect = async () => {
|
||||
// Refresh manager state
|
||||
// Store original toast setting value
|
||||
const originalToastSetting = settingStore.get(
|
||||
'Comfy.Toast.DisableReconnectingToast'
|
||||
)
|
||||
|
||||
comfyManagerStore.clearLogs()
|
||||
comfyManagerStore.setStale()
|
||||
try {
|
||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||
|
||||
// Refresh node definitions
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
isRestarting.value = true
|
||||
|
||||
// Reload workflow
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
const onReconnect = async () => {
|
||||
try {
|
||||
comfyManagerStore.setStale()
|
||||
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
await useWorkflowService().reloadCurrentWorkflow()
|
||||
} finally {
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
closeDialog()
|
||||
comfyManagerStore.resetTaskState()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
} catch (error) {
|
||||
// If restart fails, restore settings and reset state
|
||||
await settingStore.set(
|
||||
'Comfy.Toast.DisableReconnectingToast',
|
||||
originalToastSetting
|
||||
)
|
||||
isRestarting.value = false
|
||||
isRestartCompleted.value = false
|
||||
closeDialog() // Close dialog on error
|
||||
throw error
|
||||
}
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
closeDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,16 +18,40 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const activeTabIndex = ref(0)
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const activeTabIndex = computed({
|
||||
get: () => progressDialogContent.getActiveTabIndex(),
|
||||
set: (value: number) => progressDialogContent.setActiveTabIndex(value)
|
||||
})
|
||||
const { t } = useI18n()
|
||||
const tabs = [
|
||||
{ label: t('manager.installationQueue') },
|
||||
{ label: t('manager.failed', { count: 0 }) }
|
||||
]
|
||||
|
||||
const failedCount = computed(() => comfyManagerStore.failedTasksIds.length)
|
||||
|
||||
const queueSuffix = computed(() => {
|
||||
const queueLength = comfyManagerStore.managerQueue.queueLength
|
||||
if (queueLength === 0) {
|
||||
return ''
|
||||
}
|
||||
return ` (${queueLength})`
|
||||
})
|
||||
const failedSuffix = computed(() => {
|
||||
if (failedCount.value === 0) {
|
||||
return ''
|
||||
}
|
||||
return ` (${failedCount.value})`
|
||||
})
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ label: t('manager.installationQueue') + queueSuffix.value },
|
||||
{ label: t('manager.failed') + failedSuffix.value }
|
||||
])
|
||||
</script>
|
||||
|
||||
65
src/composables/nodePack/useUpdateAvailableNodes.ts
Normal file
65
src/composables/nodePack/useUpdateAvailableNodes.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Composable to find NodePacks that have updates available
|
||||
* Uses the same filtering approach as ManagerDialogContent.vue
|
||||
* Automatically fetches installed pack data when initialized
|
||||
*/
|
||||
export const useUpdateAvailableNodes = () => {
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { installedPacks, isLoading, error, startFetchInstalled } =
|
||||
useInstalledPacks()
|
||||
|
||||
// Check if a pack has updates available (same logic as usePackUpdateStatus)
|
||||
const isOutdatedPack = (pack: components['schemas']['Node']) => {
|
||||
const isInstalled = comfyManagerStore.isPackInstalled(pack?.id)
|
||||
if (!isInstalled) return false
|
||||
|
||||
const installedVersion = comfyManagerStore.getInstalledPackVersion(
|
||||
pack.id ?? ''
|
||||
)
|
||||
const latestVersion = pack.latest_version?.version
|
||||
|
||||
const isNightlyPack = !!installedVersion && !isSemVer(installedVersion)
|
||||
|
||||
if (isNightlyPack || !latestVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
return compareVersions(latestVersion, installedVersion) > 0
|
||||
}
|
||||
|
||||
// Same filtering logic as ManagerDialogContent.vue
|
||||
const filterOutdatedPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter(isOutdatedPack)
|
||||
|
||||
// Filter only outdated packs from installed packs
|
||||
const updateAvailableNodePacks = computed(() => {
|
||||
if (!installedPacks.value.length) return []
|
||||
return filterOutdatedPacks(installedPacks.value)
|
||||
})
|
||||
|
||||
// Check if there are any outdated packs
|
||||
const hasUpdateAvailable = computed(() => {
|
||||
return updateAvailableNodePacks.value.length > 0
|
||||
})
|
||||
|
||||
// Automatically fetch installed pack data when composable is used
|
||||
onMounted(async () => {
|
||||
if (!installedPacks.value.length && !isLoading.value) {
|
||||
await startFetchInstalled()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
updateAvailableNodePacks,
|
||||
hasUpdateAvailable,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { app } from '@/scripts/app'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { SelectedVersion, UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type WorkflowPack = {
|
||||
@@ -65,8 +65,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
return {
|
||||
id: CORE_NODES_PACK_NAME,
|
||||
version:
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ??
|
||||
SelectedVersion.NIGHTLY
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ?? 'nightly'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +75,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
if (pack) {
|
||||
return {
|
||||
id: pack.id,
|
||||
version: pack.latest_version?.version ?? SelectedVersion.NIGHTLY
|
||||
version: pack.latest_version?.version ?? 'nightly'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { addFluxKontextGroupNode } from '@/scripts/fluxKontextEditNode'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { type ComfyCommand, useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
@@ -29,6 +30,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
const moveSelectedNodesVersionAdded = '1.22.2'
|
||||
|
||||
@@ -660,12 +662,54 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager',
|
||||
icon: 'pi pi-puzzle',
|
||||
label: 'Toggle the Custom Nodes Manager',
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
icon: 'pi pi-objects-column',
|
||||
label: 'Custom Nodes Manager',
|
||||
versionAdded: '1.12.10',
|
||||
function: async () => {
|
||||
const { is_legacy_manager_ui } =
|
||||
(await useComfyManagerService().isLegacyManagerUI()) ?? {}
|
||||
|
||||
if (is_legacy_manager_ui === true) {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
} else {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowUpdateAvailablePacks',
|
||||
icon: 'pi pi-sync',
|
||||
label: 'Check for Custom Node Updates',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.toggleManagerDialog()
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.UpdateAvailable
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowMissingPacks',
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
label: 'Install Missing Custom Nodes',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -754,9 +798,88 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
|
||||
icon: 'pi pi-bars',
|
||||
label: 'Custom Nodes (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
|
||||
)
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ShowLegacyManagerMenu',
|
||||
icon: 'mdi mdi-puzzle',
|
||||
label: 'Manager Menu (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModels',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModels'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: false })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.UnloadModelsAndExecutionCache',
|
||||
icon: 'mdi mdi-vacuum-outline',
|
||||
label: 'Unload Models and Execution Cache',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await api.freeMemory({ freeExecutionCache: true })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,101 +1,145 @@
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { computed, readonly, ref } from 'vue'
|
||||
import { pickBy } from 'lodash'
|
||||
import { Ref, computed, ref } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { ManagerWsQueueStatus } from '@/types/comfyManagerTypes'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
type QueuedTask<T> = {
|
||||
task: () => Promise<T>
|
||||
onComplete?: () => void
|
||||
}
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
components['schemas']['TaskHistoryItem']
|
||||
>
|
||||
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
|
||||
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
|
||||
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
|
||||
type QueueTaskItem = components['schemas']['QueueTaskItem']
|
||||
type HistoryTaskItem = components['schemas']['TaskHistoryItem']
|
||||
type Task = QueueTaskItem | HistoryTaskItem
|
||||
|
||||
const MANAGER_WS_MSG_TYPE = 'cm-queue-status'
|
||||
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
|
||||
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
|
||||
|
||||
export const useManagerQueue = () => {
|
||||
const clientQueueItems = ref<QueuedTask<unknown>[]>([])
|
||||
const clientQueueLength = computed(() => clientQueueItems.value.length)
|
||||
const onCompletedQueue = ref<((() => void) | undefined)[]>([])
|
||||
const onCompleteWaitingCount = ref(0)
|
||||
const uncompletedCount = computed(
|
||||
() => clientQueueLength.value + onCompleteWaitingCount.value
|
||||
export const useManagerQueue = (
|
||||
taskHistory: Ref<ManagerTaskHistory>,
|
||||
taskQueue: Ref<ManagerTaskQueue>,
|
||||
installedPacks: Ref<Record<string, any>>
|
||||
) => {
|
||||
const { showManagerProgressDialog } = useDialogService()
|
||||
|
||||
// Task queue state (read-only from server)
|
||||
const maxHistoryItems = ref(64)
|
||||
const isLoading = ref(false)
|
||||
const isProcessing = ref(false)
|
||||
|
||||
// Computed values
|
||||
const currentQueueLength = computed(
|
||||
() =>
|
||||
taskQueue.value.running_queue.length +
|
||||
taskQueue.value.pending_queue.length
|
||||
)
|
||||
|
||||
const serverQueueStatus = ref<ManagerWsQueueStatus>(ManagerWsQueueStatus.DONE)
|
||||
const isServerIdle = computed(
|
||||
() => serverQueueStatus.value === ManagerWsQueueStatus.DONE
|
||||
)
|
||||
/**
|
||||
* Update the processing state based on the current queue length.
|
||||
* If the queue is empty, or all tasks in the queue are associated
|
||||
* with different clients, then this client is not processing any tasks.
|
||||
*/
|
||||
const updateProcessingState = (): void => {
|
||||
isProcessing.value = currentQueueLength.value > 0
|
||||
}
|
||||
|
||||
const allTasksDone = computed(
|
||||
() => isServerIdle.value && clientQueueLength.value === 0
|
||||
)
|
||||
const nextTaskReady = computed(
|
||||
() => isServerIdle.value && clientQueueLength.value > 0
|
||||
)
|
||||
const allTasksDone = computed(() => currentQueueLength.value === 0)
|
||||
const historyCount = computed(() => Object.keys(taskHistory.value).length)
|
||||
|
||||
const cleanupListener = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_MSG_TYPE,
|
||||
(event: CustomEvent<{ status: ManagerWsQueueStatus }>) => {
|
||||
if (event?.type === MANAGER_WS_MSG_TYPE && event.detail?.status) {
|
||||
serverQueueStatus.value = event.detail.status
|
||||
/**
|
||||
* Check if a task is associated with this client.
|
||||
* Task can be from running queue, pending queue, or history.
|
||||
* @param task - The task to check
|
||||
* @returns True if the task belongs to this client
|
||||
*/
|
||||
const isTaskFromThisClient = (task: Task): boolean =>
|
||||
task.client_id === app.api.clientId
|
||||
|
||||
/**
|
||||
* Filter queue tasks by client id.
|
||||
* Ensures that only tasks associated with this client are processed and
|
||||
* added to client state.
|
||||
* @param tasks - Array of queue tasks to filter
|
||||
* @returns Filtered array containing only tasks from this client
|
||||
*/
|
||||
const filterQueueByClientId = (tasks: QueueTaskItem[]): QueueTaskItem[] =>
|
||||
tasks.filter(isTaskFromThisClient)
|
||||
|
||||
/**
|
||||
* Filter history tasks by client id using lodash pickBy for optimal performance.
|
||||
* Returns a new object containing only tasks associated with this client.
|
||||
* @param history - The history object to filter
|
||||
* @returns Filtered history object containing only tasks from this client
|
||||
*/
|
||||
const filterHistoryByClientId = (history: ManagerTaskHistory) =>
|
||||
pickBy(history, isTaskFromThisClient)
|
||||
|
||||
/**
|
||||
* Update task queue and history state with filtered data from server.
|
||||
* Ensures only tasks from this client are stored in local state.
|
||||
* @param state - The task state message from the server
|
||||
*/
|
||||
const updateTaskState = (state: ManagerTaskQueue) => {
|
||||
taskQueue.value.running_queue = filterQueueByClientId(state.running_queue)
|
||||
taskQueue.value.pending_queue = filterQueueByClientId(state.pending_queue)
|
||||
taskHistory.value = filterHistoryByClientId(state.history)
|
||||
|
||||
if (state.installed_packs) {
|
||||
installedPacks.value = state.installed_packs
|
||||
}
|
||||
updateProcessingState()
|
||||
}
|
||||
|
||||
// WebSocket event listener for task done
|
||||
const cleanupTaskDoneListener = useEventListener(
|
||||
app.api,
|
||||
MANAGER_WS_TASK_DONE_NAME,
|
||||
(event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_DONE_NAME) {
|
||||
const { state } = event.detail
|
||||
updateTaskState(state)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const startNextTask = () => {
|
||||
const nextTask = clientQueueItems.value.shift()
|
||||
if (!nextTask) return
|
||||
|
||||
const { task, onComplete } = nextTask
|
||||
if (onComplete) {
|
||||
// Set the task's onComplete to be executed the next time the server is idle
|
||||
onCompletedQueue.value.push(onComplete)
|
||||
onCompleteWaitingCount.value++
|
||||
}
|
||||
|
||||
task().catch((e) => {
|
||||
const message = `Error enqueuing task for ComfyUI Manager: ${e}`
|
||||
console.error(message)
|
||||
})
|
||||
}
|
||||
|
||||
const enqueueTask = <T>(task: QueuedTask<T>): void => {
|
||||
clientQueueItems.value.push(task)
|
||||
}
|
||||
|
||||
const clearQueue = () => {
|
||||
clientQueueItems.value = []
|
||||
onCompletedQueue.value = []
|
||||
onCompleteWaitingCount.value = 0
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
clearQueue()
|
||||
cleanupListener()
|
||||
}
|
||||
|
||||
whenever(nextTaskReady, startNextTask)
|
||||
whenever(isServerIdle, () => {
|
||||
if (onCompletedQueue.value?.length) {
|
||||
while (
|
||||
onCompleteWaitingCount.value > 0 &&
|
||||
onCompletedQueue.value.length > 0
|
||||
) {
|
||||
const onComplete = onCompletedQueue.value.shift()
|
||||
onComplete?.()
|
||||
onCompleteWaitingCount.value--
|
||||
// WebSocket event listener for task started
|
||||
const cleanupTaskStartedListener = useEventListener(
|
||||
app.api,
|
||||
MANAGER_WS_TASK_STARTED_NAME,
|
||||
(event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_STARTED_NAME) {
|
||||
const { state } = event.detail
|
||||
updateTaskState(state)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
whenever(currentQueueLength, () => showManagerProgressDialog())
|
||||
|
||||
const stopListening = () => {
|
||||
cleanupTaskDoneListener()
|
||||
cleanupTaskStartedListener()
|
||||
}
|
||||
|
||||
return {
|
||||
allTasksDone,
|
||||
statusMessage: readonly(serverQueueStatus),
|
||||
queueLength: clientQueueLength,
|
||||
uncompletedCount,
|
||||
// Queue state (read-only from server)
|
||||
taskHistory,
|
||||
taskQueue,
|
||||
maxHistoryItems,
|
||||
isLoading,
|
||||
|
||||
enqueueTask,
|
||||
clearQueue,
|
||||
cleanup
|
||||
// Computed state
|
||||
allTasksDone,
|
||||
isProcessing,
|
||||
queueLength: currentQueueLength,
|
||||
historyCount,
|
||||
|
||||
// Actions
|
||||
stopListening
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { LogsWsMessage } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
const LOGS_MESSAGE_TYPE = 'logs'
|
||||
const MANAGER_WS_TASK_DONE_NAME = 'cm-task-completed'
|
||||
const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
|
||||
|
||||
type ManagerWsTaskDoneMsg = components['schemas']['MessageTaskDone']
|
||||
type ManagerWsTaskStartedMsg = components['schemas']['MessageTaskStarted']
|
||||
|
||||
interface UseServerLogsOptions {
|
||||
ui_id: string
|
||||
immediate?: boolean
|
||||
messageFilter?: (message: string) => boolean
|
||||
}
|
||||
|
||||
export const useServerLogs = (options: UseServerLogsOptions = {}) => {
|
||||
export const useServerLogs = (options: UseServerLogsOptions) => {
|
||||
const {
|
||||
immediate = false,
|
||||
messageFilter = (msg: string) => Boolean(msg.trim())
|
||||
} = options
|
||||
|
||||
const logs = ref<string[]>([])
|
||||
let stop: ReturnType<typeof useEventListener> | null = null
|
||||
const isTaskStarted = ref(false)
|
||||
let stopLogs: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskDone: ReturnType<typeof useEventListener> | null = null
|
||||
let stopTaskStarted: ReturnType<typeof useEventListener> | null = null
|
||||
|
||||
const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) =>
|
||||
event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0
|
||||
@@ -27,34 +37,81 @@ export const useServerLogs = (options: UseServerLogsOptions = {}) => {
|
||||
event.detail.entries.map((e) => e.m).filter(messageFilter)
|
||||
|
||||
const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => {
|
||||
// Only capture logs if this task has started
|
||||
if (!isTaskStarted.value) return
|
||||
|
||||
if (isValidLogEvent(event)) {
|
||||
logs.value.push(...parseLogMessage(event))
|
||||
const messages = parseLogMessage(event)
|
||||
if (messages.length > 0) {
|
||||
logs.value.push(...messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
const handleTaskStarted = (event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_STARTED_NAME) {
|
||||
// Check if this is our task starting
|
||||
const isOurTask = event.detail.ui_id === options.ui_id
|
||||
if (isOurTask) {
|
||||
isTaskStarted.value = true
|
||||
void stopTaskStarted?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskDone = (event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||
if (event?.type === MANAGER_WS_TASK_DONE_NAME) {
|
||||
const { state } = event.detail
|
||||
// Check if our task is now in the history (completed)
|
||||
const isOurTaskDone = state.history[options.ui_id]
|
||||
if (isOurTaskDone) {
|
||||
isTaskStarted.value = false
|
||||
void stopListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startListening = async () => {
|
||||
await api.subscribeLogs(true)
|
||||
stop = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
stopLogs = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
|
||||
stopTaskStarted = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_STARTED_NAME,
|
||||
handleTaskStarted
|
||||
)
|
||||
stopTaskDone = useEventListener(
|
||||
api,
|
||||
MANAGER_WS_TASK_DONE_NAME,
|
||||
handleTaskDone
|
||||
)
|
||||
}
|
||||
|
||||
const stopListening = async () => {
|
||||
stop?.()
|
||||
stop = null
|
||||
await api.subscribeLogs(false)
|
||||
stopLogs?.()
|
||||
stopTaskStarted?.()
|
||||
stopTaskDone?.()
|
||||
stopLogs = null
|
||||
stopTaskStarted = null
|
||||
stopTaskDone = null
|
||||
// TODO: move subscribe/unsubscribe logs to useManagerQueue. Subscribe when task starts if not already subscribed.
|
||||
// Unsubscribe ONLY when there are no tasks running or queued up and the only remaining task finishes.
|
||||
// await api.subscribeLogs(false)
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
void start()
|
||||
void startListening()
|
||||
}
|
||||
|
||||
onUnmounted(async () => {
|
||||
const cleanup = async () => {
|
||||
await stopListening()
|
||||
logs.value = []
|
||||
})
|
||||
isTaskStarted.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
logs,
|
||||
startListening: start,
|
||||
stopListening
|
||||
startListening,
|
||||
stopListening,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,24 @@ export const CORE_MENU_COMMANDS = [
|
||||
]
|
||||
],
|
||||
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
|
||||
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
|
||||
[
|
||||
['Edit'],
|
||||
[
|
||||
'Comfy.RefreshNodeDefinitions',
|
||||
'Comfy.Memory.UnloadModels',
|
||||
'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
]
|
||||
],
|
||||
[['Edit'], ['Comfy.ClearWorkflow']],
|
||||
[['Edit'], ['Comfy.OpenClipspace']],
|
||||
[
|
||||
['Manager'],
|
||||
[
|
||||
'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
|
||||
'Comfy.Manager.ShowMissingPacks',
|
||||
'Comfy.Manager.ShowUpdateAvailablePacks'
|
||||
]
|
||||
],
|
||||
[
|
||||
['Help'],
|
||||
[
|
||||
|
||||
@@ -14,6 +14,13 @@ import type { SettingParams } from '@/types/settingTypes'
|
||||
* when they are no longer needed.
|
||||
*/
|
||||
export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'Comfy.Memory.AllowManualUnload',
|
||||
name: 'Allow manual unload of models and execution cache via user command',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Validation.Workflows',
|
||||
name: 'Validate workflows',
|
||||
@@ -884,5 +891,12 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Release seen timestamp',
|
||||
type: 'hidden',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Memory.AllowManualUnload',
|
||||
name: 'Allow manual unload of models and execution cache via user command',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.18.0'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Load Default Workflow"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Toggle the Custom Nodes Manager"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Install Missing"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Check for Updates"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Toggle the Custom Nodes Manager Progress Bar"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Open Mask Editor for Selected Node"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Unload Models"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Unload Models and Execution Cache"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "New Blank Workflow"
|
||||
},
|
||||
|
||||
@@ -133,10 +133,14 @@
|
||||
"copyURL": "Copy URL",
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on"
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"commandProhibited": "Command {command} is prohibited. Contact an administrator for more information."
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"legacyMenuNotAvailable": "Legacy manager menu is not available, defaulting to the new manager menu.",
|
||||
"legacyManagerUI": "Use Legacy UI",
|
||||
"legacyManagerUIDescription": "To use the legacy Manager UI, start ComfyUI with --enable-manager-legacy-ui",
|
||||
"failed": "Failed ({count})",
|
||||
"noNodesFound": "No nodes found",
|
||||
"noNodesFoundDescription": "The pack's nodes either could not be parsed, or the pack is a frontend extension only and doesn't have any nodes.",
|
||||
@@ -913,6 +917,7 @@
|
||||
"menuLabels": {
|
||||
"Workflow": "Workflow",
|
||||
"Edit": "Edit",
|
||||
"Manager": "Manager",
|
||||
"Help": "Help",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Open Custom Nodes Folder": "Open Custom Nodes Folder",
|
||||
@@ -968,6 +973,14 @@
|
||||
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
|
||||
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
|
||||
"Custom Nodes (Beta)": "Custom Nodes (Beta)",
|
||||
"Custom Nodes (Legacy)": "Custom Nodes (Legacy)",
|
||||
"Manager Menu (Legacy)": "Manager Menu (Legacy)",
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"Install Missing": "Install Missing",
|
||||
"Toggle Progress Dialog": "Toggle Progress Dialog",
|
||||
"Unload Models": "Unload Models",
|
||||
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Open": "Open",
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Cargar flujo de trabajo predeterminado"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Administrador de nodos personalizados"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nodos personalizados (Beta)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Instalar faltantes"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Buscar actualizaciones"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Alternar diálogo de progreso del administrador"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Abrir editor de máscara para el nodo seleccionado"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Descargar modelos"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Descargar modelos y caché de ejecución"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nuevo flujo de trabajo en blanco"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "Color",
|
||||
"comingSoon": "Próximamente",
|
||||
"command": "Comando",
|
||||
"commandProhibited": "El comando {command} está prohibido. Contacta a un administrador para más información.",
|
||||
"community": "Comunidad",
|
||||
"completed": "Completado",
|
||||
"confirm": "Confirmar",
|
||||
@@ -628,6 +629,9 @@
|
||||
"installationQueue": "Cola de Instalación",
|
||||
"lastUpdated": "Última Actualización",
|
||||
"latestVersion": "Última",
|
||||
"legacyManagerUI": "Usar UI antigua",
|
||||
"legacyManagerUIDescription": "Para usar la UI antigua del Manager, inicia ComfyUI con --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "El menú del administrador antiguo no está disponible en esta versión de ComfyUI. Por favor, utiliza el nuevo menú del administrador en su lugar.",
|
||||
"license": "Licencia",
|
||||
"loadingVersions": "Cargando versiones...",
|
||||
"nightlyVersion": "Nocturna",
|
||||
@@ -734,6 +738,7 @@
|
||||
"Contact Support": "Contactar soporte",
|
||||
"Convert Selection to Subgraph": "Convertir selección en subgrafo",
|
||||
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
||||
"Custom Nodes Manager": "Administrador de Nodos Personalizados",
|
||||
"Delete Selected Items": "Eliminar elementos seleccionados",
|
||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
|
||||
@@ -745,6 +750,7 @@
|
||||
"Give Feedback": "Dar retroalimentación",
|
||||
"Group Selected Nodes": "Agrupar nodos seleccionados",
|
||||
"Help": "Ayuda",
|
||||
"Install Missing": "Instalar Faltantes",
|
||||
"Interrupt": "Interrumpir",
|
||||
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
|
||||
"Manage group nodes": "Gestionar nodos de grupo",
|
||||
@@ -752,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
|
||||
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
|
||||
"Move Selected Nodes Up": "Mover nodos seleccionados hacia arriba",
|
||||
"Manager": "Administrador",
|
||||
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
|
||||
"New": "Nuevo",
|
||||
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
|
||||
@@ -796,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
|
||||
"Undo": "Deshacer",
|
||||
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
|
||||
"Unload Models": "Descargar modelos",
|
||||
"Unload Models and Execution Cache": "Descargar modelos y caché de ejecución",
|
||||
"Workflow": "Flujo de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Charger le flux de travail par défaut"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Gestionnaire de Nœuds Personnalisés"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Nœuds personnalisés (Beta)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Installer manquants"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Vérifier les mises à jour"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Basculer la boîte de dialogue de progression"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Ouvrir l'éditeur de masque pour le nœud sélectionné"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Décharger les modèles"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Décharger les modèles et le cache d'exécution"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nouveau flux de travail vierge"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "Couleur",
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
"commandProhibited": "La commande {command} est interdite. Contactez un administrateur pour plus d'informations.",
|
||||
"community": "Communauté",
|
||||
"completed": "Terminé",
|
||||
"confirm": "Confirmer",
|
||||
@@ -628,6 +629,9 @@
|
||||
"installationQueue": "File d'attente d'installation",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"latestVersion": "Dernière",
|
||||
"legacyManagerUI": "Utiliser l'interface utilisateur héritée",
|
||||
"legacyManagerUIDescription": "Pour utiliser l'interface utilisateur de gestion héritée, démarrez ComfyUI avec --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Le menu du gestionnaire de l'ancienne version n'est pas disponible dans cette version de ComfyUI. Veuillez utiliser le nouveau menu du gestionnaire à la place.",
|
||||
"license": "Licence",
|
||||
"loadingVersions": "Chargement des versions...",
|
||||
"nightlyVersion": "Nocturne",
|
||||
@@ -721,7 +725,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
||||
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Check for Updates": "Vérifier les Mises à Jour",
|
||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||
"Clear Workflow": "Effacer le flux de travail",
|
||||
"Clipspace": "Espace de clip",
|
||||
@@ -734,6 +738,7 @@
|
||||
"Contact Support": "Contacter le support",
|
||||
"Convert Selection to Subgraph": "Convertir la sélection en sous-graphe",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
@@ -745,6 +750,7 @@
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Install Missing": "Installer Manquants",
|
||||
"Interrupt": "Interrompre",
|
||||
"Load Default Workflow": "Charger le flux de travail par défaut",
|
||||
"Manage group nodes": "Gérer les nœuds de groupe",
|
||||
@@ -752,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
|
||||
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
|
||||
"Move Selected Nodes Up": "Déplacer les nœuds sélectionnés vers le haut",
|
||||
"Manager": "Gestionnaire",
|
||||
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
|
||||
"New": "Nouveau",
|
||||
"Next Opened Workflow": "Prochain flux de travail ouvert",
|
||||
@@ -796,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
|
||||
"Undo": "Annuler",
|
||||
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
|
||||
"Unload Models": "Décharger les modèles",
|
||||
"Unload Models and Execution Cache": "Décharger les modèles et le cache d'exécution",
|
||||
"Workflow": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "デフォルトのワークフローを読み込む"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "カスタムノードマネージャ"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "カスタムノード(ベータ版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "不足しているパックをインストール"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "更新を確認"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "プログレスダイアログの切り替え"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "選択したノードのマスクエディタを開く"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "モデルのアンロード"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "モデルと実行キャッシュのアンロード"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新しい空のワークフロー"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "色",
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
"commandProhibited": "コマンド {command} は禁止されています。詳細は管理者にお問い合わせください。",
|
||||
"community": "コミュニティ",
|
||||
"completed": "完了",
|
||||
"confirm": "確認",
|
||||
@@ -628,6 +629,9 @@
|
||||
"installationQueue": "インストールキュー",
|
||||
"lastUpdated": "最終更新日",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "レガシーUIを使用する",
|
||||
"legacyManagerUIDescription": "レガシーManager UIを使用するには、--enable-manager-legacy-uiを付けてComfyUIを起動してください",
|
||||
"legacyMenuNotAvailable": "このバージョンのComfyUIでは、レガシーマネージャーメニューは利用できません。新しいマネージャーメニューを使用してください。",
|
||||
"license": "ライセンス",
|
||||
"loadingVersions": "バージョンを読み込んでいます...",
|
||||
"nightlyVersion": "ナイトリー",
|
||||
@@ -721,7 +725,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||
"Check for Updates": "更新を確認する",
|
||||
"Check for Updates": "更新を確認",
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
"Clear Workflow": "ワークフローをクリア",
|
||||
"Clipspace": "クリップスペース",
|
||||
@@ -734,6 +738,7 @@
|
||||
"Contact Support": "サポートに連絡",
|
||||
"Convert Selection to Subgraph": "選択範囲をサブグラフに変換",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Custom Nodes Manager": "カスタムノードマネージャ",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
@@ -745,6 +750,7 @@
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Install Missing": "不足しているものをインストール",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "デフォルトワークフローを読み込む",
|
||||
"Manage group nodes": "グループノードを管理",
|
||||
@@ -752,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "選択したノードを左へ移動",
|
||||
"Move Selected Nodes Right": "選択したノードを右へ移動",
|
||||
"Move Selected Nodes Up": "選択したノードを上へ移動",
|
||||
"Manager": "マネージャー",
|
||||
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
|
||||
"New": "新規",
|
||||
"Next Opened Workflow": "次に開いたワークフロー",
|
||||
@@ -796,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
|
||||
"Undo": "元に戻す",
|
||||
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
|
||||
"Unload Models": "モデルのアンロード",
|
||||
"Unload Models and Execution Cache": "モデルと実行キャッシュのアンロード",
|
||||
"Workflow": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "기본 워크플로 로드"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "사용자 정의 노드 관리자"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "사용자 정의 노드 (베타)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "누락된 팩 설치"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "업데이트 확인"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "진행 상황 대화 상자 전환"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "선택한 노드 마스크 편집기 열기"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "모델 언로드"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "모델 및 실행 캐시 언로드"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "새로운 빈 워크플로"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "색상",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
"commandProhibited": "명령 {command}은 금지되었습니다. 자세한 정보는 관리자에게 문의하십시오.",
|
||||
"community": "커뮤니티",
|
||||
"completed": "완료됨",
|
||||
"confirm": "확인",
|
||||
@@ -628,6 +629,9 @@
|
||||
"installationQueue": "설치 대기열",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"latestVersion": "최신",
|
||||
"legacyManagerUI": "레거시 UI 사용",
|
||||
"legacyManagerUIDescription": "레거시 매니저 UI를 사용하려면, ComfyUI를 --enable-manager-legacy-ui로 시작하세요",
|
||||
"legacyMenuNotAvailable": "이 버전의 ComfyUI에서는 레거시 매니저 메뉴를 사용할 수 없습니다. 대신 새로운 매니저 메뉴를 사용하십시오.",
|
||||
"license": "라이선스",
|
||||
"loadingVersions": "버전 로딩 중...",
|
||||
"nightlyVersion": "최신 테스트 버전(nightly)",
|
||||
@@ -734,6 +738,7 @@
|
||||
"Contact Support": "고객 지원 문의",
|
||||
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Custom Nodes Manager": "사용자 정의 노드 관리자",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
@@ -745,6 +750,7 @@
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Install Missing": "누락된 설치",
|
||||
"Interrupt": "중단",
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
@@ -752,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
|
||||
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
|
||||
"Move Selected Nodes Up": "선택한 노드 위로 이동",
|
||||
"Manager": "매니저",
|
||||
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
@@ -796,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
|
||||
"Unload Models": "모델 언로드",
|
||||
"Unload Models and Execution Cache": "모델 및 실행 캐시 언로드",
|
||||
"Workflow": "워크플로",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Загрузить стандартный рабочий процесс"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Менеджер Пользовательских Узлов"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "Пользовательские узлы (Бета)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "Установить отсутствующие"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "Проверить наличие обновлений"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Переключить диалоговое окно прогресса"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "Открыть редактор масок для выбранной ноды"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "Выгрузить модели"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "Выгрузить модели и кэш выполнения"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Новый пустой рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "Цвет",
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
"commandProhibited": "Команда {command} запрещена. Свяжитесь с администратором для получения дополнительной информации.",
|
||||
"community": "Сообщество",
|
||||
"completed": "Завершено",
|
||||
"confirm": "Подтвердить",
|
||||
@@ -628,6 +629,9 @@
|
||||
"installationQueue": "Очередь установки",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"latestVersion": "Последняя",
|
||||
"legacyManagerUI": "Использовать устаревший UI",
|
||||
"legacyManagerUIDescription": "Чтобы использовать устаревший UI менеджера, запустите ComfyUI с --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "Устаревшее меню менеджера недоступно в этой версии ComfyUI. Пожалуйста, используйте новое меню менеджера.",
|
||||
"license": "Лицензия",
|
||||
"loadingVersions": "Загрузка версий...",
|
||||
"nightlyVersion": "Ночная",
|
||||
@@ -721,7 +725,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Check for Updates": "Проверить наличие обновлений",
|
||||
"Check for Updates": "Проверить Обновления",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
"Clear Workflow": "Очистить рабочий процесс",
|
||||
"Clipspace": "Клиппространство",
|
||||
@@ -734,6 +738,7 @@
|
||||
"Contact Support": "Связаться с поддержкой",
|
||||
"Convert Selection to Subgraph": "Преобразовать выделенное в подграф",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
@@ -745,6 +750,7 @@
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Install Missing": "Установить Отсутствующие",
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
@@ -752,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
|
||||
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
|
||||
"Move Selected Nodes Up": "Переместить выбранные узлы вверх",
|
||||
"Manager": "Менеджер",
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
@@ -796,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
|
||||
"Undo": "Отменить",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
"Unload Models": "Выгрузить модели",
|
||||
"Unload Models and Execution Cache": "Выгрузить модели и кэш выполнения",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить"
|
||||
|
||||
@@ -152,8 +152,20 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "載入預設工作流程"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "切換自訂節點管理器"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "顯示自訂節點管理器"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "自訂節點(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "管理選單(舊版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安裝缺少的自訂節點"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "檢查自訂節點更新"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切換自訂節點管理器進度條"
|
||||
@@ -161,6 +173,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "為選取的節點開啟 Mask 編輯器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸載模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸載模型與執行快取"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新增空白工作流程"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "顏色",
|
||||
"comingSoon": "即將推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "指令 {command} 已被禁止。如需更多資訊,請聯絡管理員。",
|
||||
"community": "社群",
|
||||
"completed": "已完成",
|
||||
"confirm": "確認",
|
||||
@@ -609,12 +610,14 @@
|
||||
"title": "維護"
|
||||
},
|
||||
"manager": {
|
||||
"applyChanges": "套用變更",
|
||||
"changingVersion": "正在將版本從 {from} 變更為 {to}",
|
||||
"createdBy": "建立者",
|
||||
"dependencies": "相依套件",
|
||||
"discoverCommunityContent": "探索社群製作的節點包、擴充功能等...",
|
||||
"downloads": "下載次數",
|
||||
"errorConnecting": "連線至 Comfy Node Registry 時發生錯誤。",
|
||||
"extensionsSuccessfullyInstalled": "擴充功能安裝成功,已可使用!",
|
||||
"failed": "失敗({count})",
|
||||
"filter": {
|
||||
"disabled": "已停用",
|
||||
@@ -626,8 +629,12 @@
|
||||
"installAllMissingNodes": "安裝所有缺少的節點",
|
||||
"installSelected": "安裝所選項目",
|
||||
"installationQueue": "安裝佇列",
|
||||
"installingDependencies": "正在安裝相依套件……",
|
||||
"lastUpdated": "最後更新",
|
||||
"latestVersion": "最新版本",
|
||||
"legacyManagerUI": "使用舊版介面",
|
||||
"legacyManagerUIDescription": "若要使用舊版管理介面,請以 --enable-manager-legacy-ui 啟動 ComfyUI",
|
||||
"legacyMenuNotAvailable": "舊版管理選單不可用,已預設切換至新版管理選單。",
|
||||
"license": "授權條款",
|
||||
"loadingVersions": "正在載入版本...",
|
||||
"nightlyVersion": "每夜建置版",
|
||||
@@ -639,6 +646,7 @@
|
||||
"packsSelected": "已選擇套件",
|
||||
"repository": "儲存庫",
|
||||
"restartToApplyChanges": "請重新啟動 ComfyUI 以套用變更",
|
||||
"restartingBackend": "正在重新啟動後端以套用變更……",
|
||||
"searchPlaceholder": "搜尋",
|
||||
"selectVersion": "選擇版本",
|
||||
"sort": {
|
||||
@@ -663,6 +671,8 @@
|
||||
"uninstallSelected": "解除安裝所選項目",
|
||||
"uninstalling": "正在解除安裝",
|
||||
"update": "更新",
|
||||
"updateAll": "全部更新",
|
||||
"updateSelected": "更新所選項目",
|
||||
"updatingAllPacks": "正在更新所有套件",
|
||||
"version": "版本"
|
||||
},
|
||||
@@ -721,6 +731,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||
"Check for Custom Node Updates": "檢查自訂節點更新",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Clear Pending Tasks": "清除待處理任務",
|
||||
"Clear Workflow": "清除工作流程",
|
||||
@@ -734,6 +745,7 @@
|
||||
"Contact Support": "聯絡支援",
|
||||
"Convert Selection to Subgraph": "將選取內容轉為子圖",
|
||||
"Convert selected nodes to group node": "將選取節點轉為群組節點",
|
||||
"Custom Nodes (Legacy)": "自訂節點(舊版)",
|
||||
"Delete Selected Items": "刪除選取項目",
|
||||
"Desktop User Guide": "桌面應用程式使用指南",
|
||||
"Duplicate Current Workflow": "複製目前工作流程",
|
||||
@@ -745,9 +757,12 @@
|
||||
"Give Feedback": "提供意見回饋",
|
||||
"Group Selected Nodes": "群組選取節點",
|
||||
"Help": "說明",
|
||||
"Install Missing Custom Nodes": "安裝缺少的自訂節點",
|
||||
"Interrupt": "中斷",
|
||||
"Load Default Workflow": "載入預設工作流程",
|
||||
"Manage group nodes": "管理群組節點",
|
||||
"Manager": "管理員",
|
||||
"Manager Menu (Legacy)": "管理員選單(舊版)",
|
||||
"Move Selected Nodes Down": "選取節點下移",
|
||||
"Move Selected Nodes Left": "選取節點左移",
|
||||
"Move Selected Nodes Right": "選取節點右移",
|
||||
@@ -781,6 +796,7 @@
|
||||
"Save": "儲存",
|
||||
"Save As": "另存新檔",
|
||||
"Show Settings Dialog": "顯示設定對話框",
|
||||
"Show the Custom Nodes Manager": "顯示自訂節點管理員",
|
||||
"Sign Out": "登出",
|
||||
"Toggle Bottom Panel": "切換下方面板",
|
||||
"Toggle Focus Mode": "切換專注模式",
|
||||
@@ -792,10 +808,11 @@
|
||||
"Toggle Terminal Bottom Panel": "切換終端機底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切換主題(深色/淺色)",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager": "切換自訂節點管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
|
||||
"Undo": "復原",
|
||||
"Ungroup selected group nodes": "取消群組選取的群組節點",
|
||||
"Unload Models": "卸載模型",
|
||||
"Unload Models and Execution Cache": "卸載模型與執行快取",
|
||||
"Workflow": "工作流程",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小"
|
||||
|
||||
@@ -152,8 +152,14 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "加载默认工作流"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "自定义节点管理器"
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "自定义节点(测试版)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "安装缺失的包"
|
||||
},
|
||||
"Comfy_Manager_ShowUpdateAvailablePacks": {
|
||||
"label": "检查更新"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切换进度对话框"
|
||||
@@ -161,6 +167,12 @@
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "打开选中节点的遮罩编辑器"
|
||||
},
|
||||
"Comfy_Memory_UnloadModels": {
|
||||
"label": "卸载模型"
|
||||
},
|
||||
"Comfy_Memory_UnloadModelsAndExecutionCache": {
|
||||
"label": "卸载模型和执行缓存"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新建空白工作流"
|
||||
},
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"color": "颜色",
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
"commandProhibited": "命令 {command} 被禁止。请联系管理员获取更多信息。",
|
||||
"community": "社区",
|
||||
"completed": "已完成",
|
||||
"confirm": "确认",
|
||||
@@ -628,6 +629,9 @@
|
||||
"installationQueue": "安装队列",
|
||||
"lastUpdated": "最后更新",
|
||||
"latestVersion": "最新",
|
||||
"legacyManagerUI": "使用旧版UI",
|
||||
"legacyManagerUIDescription": "要使用旧版的管理器UI,请启动ComfyUI并使用 --enable-manager-legacy-ui",
|
||||
"legacyMenuNotAvailable": "在此版本的ComfyUI中,不提供旧版的管理器菜单。请使用新的管理器菜单。",
|
||||
"license": "许可证",
|
||||
"loadingVersions": "正在加载版本...",
|
||||
"nightlyVersion": "每夜",
|
||||
@@ -734,6 +738,7 @@
|
||||
"Contact Support": "联系支持",
|
||||
"Convert Selection to Subgraph": "将选中内容转换为子图",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Custom Nodes Manager": "自定义节点管理器",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
@@ -745,6 +750,7 @@
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Install Missing": "安装缺失",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
@@ -752,6 +758,7 @@
|
||||
"Move Selected Nodes Left": "左移所选节点",
|
||||
"Move Selected Nodes Right": "右移所选节点",
|
||||
"Move Selected Nodes Up": "上移所选节点",
|
||||
"Manager": "管理器",
|
||||
"Mute/Unmute Selected Nodes": "静音/取消静音选定节点",
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
@@ -796,6 +803,8 @@
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
"Ungroup selected group nodes": "解散选中组节点",
|
||||
"Unload Models": "卸载模型",
|
||||
"Unload Models and Execution Cache": "卸载模型和执行缓存",
|
||||
"Workflow": "工作流",
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面"
|
||||
|
||||
@@ -474,6 +474,7 @@ const zSettings = z.object({
|
||||
'Comfy.Load3D.LightIntensityMinimum': z.number(),
|
||||
'Comfy.Load3D.LightAdjustmentIncrement': z.number(),
|
||||
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
|
||||
'Comfy.Memory.AllowManualUnload': z.boolean(),
|
||||
'pysssss.SnapToGrid': z.boolean(),
|
||||
/** VHS setting is used for queue video preview support. */
|
||||
'VHS.AdvancedPreviews': z.string(),
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
type ComfyNodeDef,
|
||||
validateComfyNodeDef
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||
|
||||
interface QueuePromptRequestBody {
|
||||
@@ -987,6 +988,56 @@ export class ComfyApi extends EventTarget {
|
||||
return (await axios.get(this.internalURL('/folder_paths'))).data
|
||||
}
|
||||
|
||||
/* Frees memory by unloading models and optionally freeing execution cache
|
||||
* @param {Object} options - The options object
|
||||
* @param {boolean} options.freeExecutionCache - If true, also frees execution cache
|
||||
*/
|
||||
async freeMemory(options: { freeExecutionCache: boolean }) {
|
||||
try {
|
||||
let mode = ''
|
||||
if (options.freeExecutionCache) {
|
||||
mode = '{"unload_models": true, "free_memory": true}'
|
||||
} else {
|
||||
mode = '{"unload_models": true}'
|
||||
}
|
||||
|
||||
const res = await this.fetchApi(`/free`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: mode
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
if (options.freeExecutionCache) {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models and Execution Cache have been cleared.',
|
||||
life: 3000
|
||||
})
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: 'Models have been unloaded.',
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
} else {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'An error occurred while trying to unload models.',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom nodes i18n data from the server.
|
||||
*
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import axios, { AxiosError, AxiosResponse } from 'axios'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import {
|
||||
type InstallPackParams,
|
||||
type InstalledPacksResponse,
|
||||
type ManagerPackInfo,
|
||||
type ManagerQueueStatus,
|
||||
SelectedVersion,
|
||||
type UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
type ManagerQueueStatus = components['schemas']['QueueStatus']
|
||||
type InstallPackParams = components['schemas']['InstallPackParams']
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
|
||||
type ManagerTaskHistory = components['schemas']['HistoryResponse']
|
||||
type QueueTaskItem = components['schemas']['QueueTaskItem']
|
||||
|
||||
const GENERIC_SECURITY_ERR_MSG =
|
||||
'Forbidden: A security error has occurred. Please check the terminal logs'
|
||||
|
||||
@@ -19,20 +20,23 @@ const GENERIC_SECURITY_ERR_MSG =
|
||||
* API routes for ComfyUI Manager
|
||||
*/
|
||||
enum ManagerRoute {
|
||||
START_QUEUE = 'manager/queue/start',
|
||||
RESET_QUEUE = 'manager/queue/reset',
|
||||
QUEUE_STATUS = 'manager/queue/status',
|
||||
INSTALL = 'manager/queue/install',
|
||||
UPDATE = 'manager/queue/update',
|
||||
UPDATE_ALL = 'manager/queue/update_all',
|
||||
UNINSTALL = 'manager/queue/uninstall',
|
||||
DISABLE = 'manager/queue/disable',
|
||||
FIX_NODE = 'manager/queue/fix',
|
||||
LIST_INSTALLED = 'customnode/installed',
|
||||
GET_NODES = 'customnode/getmappings',
|
||||
GET_PACKS = 'customnode/getlist',
|
||||
IMPORT_FAIL_INFO = 'customnode/import_fail_info',
|
||||
REBOOT = 'manager/reboot'
|
||||
START_QUEUE = 'v2/manager/queue/start',
|
||||
RESET_QUEUE = 'v2/manager/queue/reset',
|
||||
QUEUE_STATUS = 'v2/manager/queue/status',
|
||||
INSTALL = 'v2/manager/queue/install',
|
||||
UPDATE = 'v2/manager/queue/update',
|
||||
UPDATE_ALL = 'v2/manager/queue/update_all',
|
||||
UNINSTALL = 'v2/manager/queue/uninstall',
|
||||
DISABLE = 'v2/manager/queue/disable',
|
||||
FIX_NODE = 'v2/manager/queue/fix',
|
||||
LIST_INSTALLED = 'v2/customnode/installed',
|
||||
GET_NODES = 'v2/customnode/getmappings',
|
||||
GET_PACKS = 'v2/customnode/getlist',
|
||||
IMPORT_FAIL_INFO = 'v2/customnode/import_fail_info',
|
||||
REBOOT = 'v2/manager/reboot',
|
||||
IS_LEGACY_MANAGER_UI = 'v2/manager/is_legacy_manager_ui',
|
||||
TASK_HISTORY = 'v2/manager/queue/history',
|
||||
QUEUE_TASK = 'v2/manager/queue/task'
|
||||
}
|
||||
|
||||
const managerApiClient = axios.create({
|
||||
@@ -49,7 +53,6 @@ const managerApiClient = axios.create({
|
||||
export const useComfyManagerService = () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const didStartQueue = ref(false)
|
||||
|
||||
const handleRequestError = (
|
||||
err: unknown,
|
||||
@@ -110,28 +113,21 @@ export const useComfyManagerService = () => {
|
||||
201: 'Created: ComfyUI-Manager job queue is already running'
|
||||
}
|
||||
|
||||
didStartQueue.value = true
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.get(ManagerRoute.START_QUEUE, { signal }),
|
||||
{ errorContext, routeSpecificErrors }
|
||||
)
|
||||
}
|
||||
|
||||
const getQueueStatus = async (signal?: AbortSignal) => {
|
||||
const getQueueStatus = async (client_id?: string, signal?: AbortSignal) => {
|
||||
const errorContext = 'Getting ComfyUI-Manager queue status'
|
||||
|
||||
return executeRequest<ManagerQueueStatus>(
|
||||
() => managerApiClient.get(ManagerRoute.QUEUE_STATUS, { signal }),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
const resetQueue = async (signal?: AbortSignal) => {
|
||||
const errorContext = 'Resetting ComfyUI-Manager queue'
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.get(ManagerRoute.RESET_QUEUE, { signal }),
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.QUEUE_STATUS, {
|
||||
params: client_id ? { client_id } : undefined,
|
||||
signal
|
||||
}),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
@@ -154,73 +150,66 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const installPack = async (
|
||||
params: InstallPackParams,
|
||||
const queueTask = async (
|
||||
kind: QueueTaskItem['kind'],
|
||||
params: QueueTaskItem['params'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = `Installing pack ${params.id}`
|
||||
const task: QueueTaskItem = {
|
||||
kind,
|
||||
params,
|
||||
ui_id: ui_id || uuidv4(),
|
||||
client_id: api.clientId ?? api.initialClientId ?? 'unknown'
|
||||
}
|
||||
|
||||
const errorContext = `Queueing ${task.kind} task`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG,
|
||||
404:
|
||||
params.selected_version === SelectedVersion.NIGHTLY
|
||||
? `Not Found: Node pack ${params.id} does not provide nightly version`
|
||||
: GENERIC_SECURITY_ERR_MSG
|
||||
404: `Not Found: Task could not be queued`
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.INSTALL, params, { signal }),
|
||||
() => managerApiClient.post(ManagerRoute.QUEUE_TASK, task, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
|
||||
const installPack = async (
|
||||
params: InstallPackParams,
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
return queueTask('install', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const uninstallPack = async (
|
||||
params: ManagerPackInfo,
|
||||
params: components['schemas']['UninstallPackParams'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = `Uninstalling pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.UNINSTALL, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
return queueTask('uninstall', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const disablePack = async (
|
||||
params: ManagerPackInfo,
|
||||
params: components['schemas']['DisablePackParams'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<null> => {
|
||||
const errorContext = `Disabling pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
404: `Pack ${params.id} not found or not installed`,
|
||||
409: `Pack ${params.id} is already disabled`
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.DISABLE, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
return queueTask('disable', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const updatePack = async (
|
||||
params: ManagerPackInfo,
|
||||
params: components['schemas']['UpdatePackParams'],
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<null> => {
|
||||
const errorContext = `Updating pack ${params.id}`
|
||||
const routeSpecificErrors = {
|
||||
403: GENERIC_SECURITY_ERR_MSG
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.post(ManagerRoute.UPDATE, params, { signal }),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
return queueTask('update', params, ui_id, signal)
|
||||
}
|
||||
|
||||
const updateAllPacks = async (
|
||||
params?: UpdateAllPacksParams,
|
||||
params: UpdateAllPacksParams = {},
|
||||
ui_id?: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = 'Updating all packs'
|
||||
@@ -229,8 +218,18 @@ export const useComfyManagerService = () => {
|
||||
401: 'Unauthorized: ComfyUI-Manager job queue is busy'
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
mode: params.mode,
|
||||
client_id: api.clientId ?? api.initialClientId ?? 'unknown',
|
||||
ui_id: ui_id || uuidv4()
|
||||
}
|
||||
|
||||
return executeRequest<null>(
|
||||
() => managerApiClient.get(ManagerRoute.UPDATE_ALL, { params, signal }),
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.UPDATE_ALL, {
|
||||
params: queryParams,
|
||||
signal
|
||||
}),
|
||||
{ errorContext, routeSpecificErrors, isQueueOperation: true }
|
||||
)
|
||||
}
|
||||
@@ -247,6 +246,36 @@ export const useComfyManagerService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const isLegacyManagerUI = async (signal?: AbortSignal) => {
|
||||
const errorContext = 'Checking if user set Manager to use the legacy UI'
|
||||
|
||||
return executeRequest<{ is_legacy_manager_ui: boolean }>(
|
||||
() => managerApiClient.get(ManagerRoute.IS_LEGACY_MANAGER_UI, { signal }),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
const getTaskHistory = async (
|
||||
options: {
|
||||
ui_id?: string
|
||||
max_items?: number
|
||||
client_id?: string
|
||||
offset?: number
|
||||
} = {},
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const errorContext = 'Getting ComfyUI-Manager task history'
|
||||
|
||||
return executeRequest<ManagerTaskHistory>(
|
||||
() =>
|
||||
managerApiClient.get(ManagerRoute.TASK_HISTORY, {
|
||||
params: options,
|
||||
signal
|
||||
}),
|
||||
{ errorContext }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
@@ -254,8 +283,8 @@ export const useComfyManagerService = () => {
|
||||
|
||||
// Queue operations
|
||||
startQueue,
|
||||
resetQueue,
|
||||
getQueueStatus,
|
||||
getTaskHistory,
|
||||
|
||||
// Pack management
|
||||
listInstalledPacks,
|
||||
@@ -268,6 +297,7 @@ export const useComfyManagerService = () => {
|
||||
updateAllPacks,
|
||||
|
||||
// System operations
|
||||
rebootComfyUI
|
||||
rebootComfyUI,
|
||||
isLegacyManagerUI
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { useServerLogs } from '@/composables/useServerLogs'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
InstallPackParams,
|
||||
InstalledPacksResponse,
|
||||
ManagerPackInfo,
|
||||
ManagerPackInstalled,
|
||||
TaskLog,
|
||||
UpdateAllPacksParams
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { TaskLog } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
type InstallPackParams = components['schemas']['InstallPackParams']
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
type ManagerPackInfo = components['schemas']['ManagerPackInfo']
|
||||
type ManagerPackInstalled = components['schemas']['ManagerPackInstalled']
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
components['schemas']['TaskHistoryItem']
|
||||
>
|
||||
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
|
||||
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
|
||||
|
||||
/**
|
||||
* Store for state of installed node packs
|
||||
@@ -31,14 +38,62 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const installedPacksIds = ref<Set<string>>(new Set())
|
||||
const isStale = ref(true)
|
||||
const taskLogs = ref<TaskLog[]>([])
|
||||
const succeededTasksLogs = ref<TaskLog[]>([])
|
||||
const failedTasksLogs = ref<TaskLog[]>([])
|
||||
|
||||
const { statusMessage, allTasksDone, enqueueTask, uncompletedCount } =
|
||||
useManagerQueue()
|
||||
const taskHistory = ref<ManagerTaskHistory>({})
|
||||
const succeededTasksIds = ref<string[]>([])
|
||||
const failedTasksIds = ref<string[]>([])
|
||||
const taskQueue = ref<ManagerTaskQueue>({
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
})
|
||||
|
||||
const managerQueue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const setStale = () => {
|
||||
isStale.value = true
|
||||
}
|
||||
|
||||
const partitionTaskLogs = () => {
|
||||
const successTaskLogs: TaskLog[] = []
|
||||
const failTaskLogs: TaskLog[] = []
|
||||
for (const log of taskLogs.value) {
|
||||
if (failedTasksIds.value.includes(log.taskId)) {
|
||||
failTaskLogs.push(log)
|
||||
} else {
|
||||
successTaskLogs.push(log)
|
||||
}
|
||||
}
|
||||
succeededTasksLogs.value = successTaskLogs
|
||||
failedTasksLogs.value = failTaskLogs
|
||||
}
|
||||
|
||||
const partitionTasks = () => {
|
||||
const successTasksIds = []
|
||||
const failTasksIds = []
|
||||
for (const task of Object.values(taskHistory.value)) {
|
||||
if (task.status?.status_str === 'success') {
|
||||
successTasksIds.push(task.ui_id)
|
||||
} else {
|
||||
failTasksIds.push(task.ui_id)
|
||||
}
|
||||
}
|
||||
succeededTasksIds.value = successTasksIds
|
||||
failedTasksIds.value = failTasksIds
|
||||
}
|
||||
|
||||
whenever(
|
||||
taskHistory,
|
||||
() => {
|
||||
partitionTasks()
|
||||
partitionTaskLogs()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const getPackId = (pack: ManagerPackInstalled) => pack.cnr_id || pack.aux_id
|
||||
|
||||
const isInstalledPackId = (packName: string | undefined): boolean =>
|
||||
@@ -97,11 +152,13 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
|
||||
const updateInstalledIds = (packs: ManagerPackInstalled[]) => {
|
||||
installedPacksIds.value = packsToIdSet(packs)
|
||||
const newIds = packsToIdSet(packs)
|
||||
installedPacksIds.value = newIds
|
||||
}
|
||||
|
||||
const onPacksChanged = () => {
|
||||
const packs = Object.values(installedPacks.value)
|
||||
|
||||
updateDisabledIds(packs)
|
||||
updateInstalledIds(packs)
|
||||
}
|
||||
@@ -115,23 +172,46 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
const enqueueTaskWithLogs = async (
|
||||
task: (taskId: string) => Promise<null>,
|
||||
taskName: string
|
||||
) => {
|
||||
const taskId = uuidv4()
|
||||
const { logs } = useServerLogs({
|
||||
ui_id: taskId,
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const loggedTask = async () => {
|
||||
taskLogs.value.push({ taskName, logs: logs.value })
|
||||
await startListening()
|
||||
return task()
|
||||
try {
|
||||
// Show progress dialog immediately when task is queued
|
||||
showManagerProgressDialog()
|
||||
managerQueue.isProcessing.value = true
|
||||
|
||||
// Prepare logging hook
|
||||
taskLogs.value.push({ taskName, taskId, logs: logs.value })
|
||||
|
||||
// Queue the task to the server
|
||||
await task(taskId)
|
||||
} catch (error) {
|
||||
// Reset processing state on error
|
||||
managerQueue.isProcessing.value = false
|
||||
|
||||
// The server has authority over task history in general, but in rare
|
||||
// case of client-side error, we add that to failed tasks from the client side
|
||||
taskHistory.value[taskId] = {
|
||||
ui_id: taskId,
|
||||
client_id: api.clientId || 'unknown',
|
||||
kind: 'error',
|
||||
result: 'failed',
|
||||
status: {
|
||||
status_str: 'error',
|
||||
completed: false,
|
||||
messages: [error instanceof Error ? error.message : String(error)]
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const onComplete = async () => {
|
||||
await stopListening()
|
||||
setStale()
|
||||
}
|
||||
|
||||
return { task: loggedTask, onComplete }
|
||||
}
|
||||
|
||||
const installPack = useCachedRequest<InstallPackParams, void>(
|
||||
@@ -152,39 +232,62 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const task = () => managerService.installPack(params, signal)
|
||||
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`))
|
||||
const task = (taskId: string) =>
|
||||
managerService.installPack(params, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, `${actionDescription} ${params.id}`)
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const uninstallPack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const uninstallPack = async (
|
||||
params: ManagerPackInfo,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
installPack.clear()
|
||||
installPack.cancel()
|
||||
const task = () => managerService.uninstallPack(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
|
||||
const uninstallParams: components['schemas']['UninstallPackParams'] = {
|
||||
node_name: params.id,
|
||||
is_unknown: false
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.uninstallPack(uninstallParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(
|
||||
task,
|
||||
t('manager.uninstalling', { id: params.id })
|
||||
)
|
||||
}
|
||||
|
||||
const updatePack = useCachedRequest<ManagerPackInfo, void>(
|
||||
async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
updateAllPacks.cancel()
|
||||
const task = () => managerService.updatePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
|
||||
const updateParams: components['schemas']['UpdatePackParams'] = {
|
||||
node_name: params.id,
|
||||
node_ver: params.version
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.updatePack(updateParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('g.updating', { id: params.id }))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>(
|
||||
async (params: UpdateAllPacksParams, signal?: AbortSignal) => {
|
||||
const task = () => managerService.updateAllPacks(params, signal)
|
||||
enqueueTask(withLogs(task, t('manager.updatingAllPacks')))
|
||||
const task = (taskId: string) =>
|
||||
managerService.updateAllPacks(params, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('manager.updatingAllPacks'))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const task = () => managerService.disablePack(params, signal)
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
|
||||
const disablePack = async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const disableParams: components['schemas']['DisablePackParams'] = {
|
||||
node_name: params.id,
|
||||
is_unknown: false
|
||||
}
|
||||
const task = (taskId: string) =>
|
||||
managerService.disablePack(disableParams, taskId, signal)
|
||||
await enqueueTaskWithLogs(task, t('g.disabling', { id: params.id }))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
@@ -196,15 +299,31 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
taskLogs.value = []
|
||||
}
|
||||
|
||||
const resetTaskState = () => {
|
||||
// Clear all task-related reactive state for fresh start after restart
|
||||
taskLogs.value = []
|
||||
taskHistory.value = {}
|
||||
succeededTasksIds.value = []
|
||||
failedTasksIds.value = []
|
||||
succeededTasksLogs.value = []
|
||||
failedTasksLogs.value = []
|
||||
|
||||
// Reset task queue to initial state
|
||||
taskQueue.value = {
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Manager state
|
||||
isLoading: managerService.isLoading,
|
||||
error: managerService.error,
|
||||
statusMessage,
|
||||
allTasksDone,
|
||||
uncompletedCount,
|
||||
taskLogs,
|
||||
clearLogs,
|
||||
resetTaskState,
|
||||
setStale,
|
||||
|
||||
// Installed packs state
|
||||
@@ -215,6 +334,15 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
getInstalledPackVersion,
|
||||
refreshInstalledList,
|
||||
|
||||
// Task queue state and actions
|
||||
taskHistory,
|
||||
isProcessingTasks: managerQueue.isProcessing,
|
||||
succeededTasksIds,
|
||||
failedTasksIds,
|
||||
succeededTasksLogs,
|
||||
failedTasksLogs,
|
||||
managerQueue,
|
||||
|
||||
// Pack actions
|
||||
installPack,
|
||||
uninstallPack,
|
||||
@@ -234,6 +362,15 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
'managerProgressDialog',
|
||||
() => {
|
||||
const isExpanded = ref(false)
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
const setActiveTabIndex = (index: number) => {
|
||||
activeTabIndex.value = index
|
||||
}
|
||||
|
||||
const getActiveTabIndex = () => {
|
||||
return activeTabIndex.value
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
@@ -250,7 +387,9 @@ export const useManagerProgressDialogStore = defineStore(
|
||||
isExpanded,
|
||||
toggle,
|
||||
collapse,
|
||||
expand
|
||||
expand,
|
||||
setActiveTabIndex,
|
||||
getActiveTabIndex
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import type { SearchMode } from '@/types/searchServiceTypes'
|
||||
|
||||
type WorkflowNodeProperties = ComfyWorkflowJSON['nodes'][0]['properties']
|
||||
|
||||
export type RegistryPack = components['schemas']['Node']
|
||||
export type MergedNodePack = RegistryPack & AlgoliaNodePack
|
||||
export const isMergedNodePack = (
|
||||
nodePack: RegistryPack | AlgoliaNodePack
|
||||
): nodePack is MergedNodePack => 'comfy_nodes' in nodePack
|
||||
|
||||
export type PackField = keyof RegistryPack | null
|
||||
|
||||
export const IsInstallingKey: InjectionKey<Ref<boolean>> =
|
||||
Symbol('isInstalling')
|
||||
|
||||
export enum ManagerWsQueueStatus {
|
||||
DONE = 'done',
|
||||
DONE = 'all-done',
|
||||
IN_PROGRESS = 'in_progress'
|
||||
}
|
||||
|
||||
@@ -52,6 +40,7 @@ export interface SearchOption<T> {
|
||||
|
||||
export type TaskLog = {
|
||||
taskName: string
|
||||
taskId: string
|
||||
logs: string[]
|
||||
}
|
||||
|
||||
@@ -60,185 +49,25 @@ export interface UseNodePacksOptions {
|
||||
maxConcurrent?: number
|
||||
}
|
||||
|
||||
enum ManagerPackState {
|
||||
/** Pack is installed and enabled */
|
||||
INSTALLED = 'installed',
|
||||
/** Pack is installed but disabled */
|
||||
DISABLED = 'disabled',
|
||||
/** Pack is not installed */
|
||||
NOT_INSTALLED = 'not_installed',
|
||||
/** Pack failed to import */
|
||||
IMPORT_FAILED = 'import_failed',
|
||||
/** Pack has an update available */
|
||||
NEEDS_UPDATE = 'needs_update'
|
||||
}
|
||||
// Node pack types from different sources
|
||||
export type RegistryPack = components['schemas']['Node']
|
||||
|
||||
enum ManagerPackInstallType {
|
||||
/** Installed via git clone */
|
||||
GIT = 'git-clone',
|
||||
/** Installed via file copy */
|
||||
COPY = 'copy',
|
||||
/** Installed from the Comfy Registry */
|
||||
REGISTRY = 'cnr'
|
||||
}
|
||||
|
||||
export enum SelectedVersion {
|
||||
/** Latest version of the pack from the registry */
|
||||
LATEST = 'latest',
|
||||
/** Latest commit of the pack from its GitHub repository */
|
||||
NIGHTLY = 'nightly'
|
||||
}
|
||||
|
||||
export enum ManagerChannel {
|
||||
/** All packs except those with instability or security issues */
|
||||
DEFAULT = 'default',
|
||||
/** Packs that were recently updated */
|
||||
RECENT = 'recent',
|
||||
/** Packs that were superseded by distinct replacements of some type */
|
||||
LEGACY = 'legacy',
|
||||
/** Packs that were forked as a result of the original pack going unmaintained */
|
||||
FORKED = 'forked',
|
||||
/** Packs with instability or security issues suitable only for developers */
|
||||
DEV = 'dev',
|
||||
/** Packs suitable for beginners */
|
||||
TUTORIAL = 'tutorial'
|
||||
}
|
||||
|
||||
export enum ManagerDatabaseSource {
|
||||
/** Get pack info from the Comfy Registry */
|
||||
REMOTE = 'remote',
|
||||
/** If set to `local`, the channel is ignored */
|
||||
LOCAL = 'local',
|
||||
/** Get pack info from the cached response from the Comfy Registry (1 day TTL) */
|
||||
CACHE = 'cache'
|
||||
}
|
||||
|
||||
export interface ManagerQueueStatus {
|
||||
/** `done_count` + `in_progress_count` + number of items queued */
|
||||
total_count: number
|
||||
/** Task worker thread is alive, a queued operation is running */
|
||||
is_processing: boolean
|
||||
/** Number of items in the queue that have been completed */
|
||||
done_count: number
|
||||
/** Number of items in the queue that are currently running */
|
||||
in_progress_count: number
|
||||
}
|
||||
|
||||
export interface ManagerPackInfo {
|
||||
/** Either github-author/github-repo or name of pack from the registry (not id) */
|
||||
id: WorkflowNodeProperties['aux_id'] | WorkflowNodeProperties['cnr_id']
|
||||
/** Semantic version or Git commit hash */
|
||||
version: WorkflowNodeProperties['ver']
|
||||
}
|
||||
|
||||
export interface ManagerPackInstalled {
|
||||
/**
|
||||
* The version of the pack that is installed.
|
||||
* Git commit hash or semantic version.
|
||||
*/
|
||||
ver: WorkflowNodeProperties['ver']
|
||||
/**
|
||||
* The name of the pack if the pack is installed from the registry.
|
||||
* Corresponds to `Node#name` in comfy-api.
|
||||
*/
|
||||
cnr_id: WorkflowNodeProperties['cnr_id']
|
||||
/**
|
||||
* The name of the pack if the pack is installed from github.
|
||||
* In the format author/repo-name. If the pack is installed from the registry, this is `null`.
|
||||
*/
|
||||
aux_id: WorkflowNodeProperties['aux_id'] | null
|
||||
enabled: boolean
|
||||
}
|
||||
// MergedNodePack is the intersection of AlgoliaNodePack and RegistryPack
|
||||
// created by lodash merge operation: merge({}, algoliaNodePack, registryPack)
|
||||
export type MergedNodePack = AlgoliaNodePack & RegistryPack
|
||||
|
||||
/**
|
||||
* Returned by `/customnode/installed`
|
||||
* Type guard to check if a node pack is from Algolia (has comfy_nodes)
|
||||
*/
|
||||
export type InstalledPacksResponse = Record<
|
||||
NonNullable<RegistryPack['name']>,
|
||||
ManagerPackInstalled
|
||||
>
|
||||
|
||||
/**
|
||||
* Returned by `/customnode/getlist`
|
||||
*/
|
||||
export interface ManagerPack extends ManagerPackInfo {
|
||||
/** Pack author name or 'Unclaimed' if the pack was added automatically via GitHub crawl. */
|
||||
author: components['schemas']['Node']['author']
|
||||
/** Files included in the pack */
|
||||
files: string[]
|
||||
/** The type of installation that was used to install the pack */
|
||||
reference: string
|
||||
/** The display name of the pack */
|
||||
title: string
|
||||
/** The latest version of the pack */
|
||||
cnr_latest: SelectedVersion
|
||||
/** The github link to the repository of the pack */
|
||||
repository: string
|
||||
/** The state of the pack */
|
||||
state: ManagerPackState
|
||||
/** The state of the pack update */
|
||||
'update-state': 'false' | 'true' | null
|
||||
/** The number of stars the pack has on GitHub. Distinct from registry stars */
|
||||
stars: number
|
||||
/**
|
||||
* The last time the pack was updated. In ISO 8601 format.
|
||||
* @example '2024-05-22 20:00:00'
|
||||
*/
|
||||
last_update: string
|
||||
health: string
|
||||
description: string
|
||||
trust: boolean
|
||||
install_type: ManagerPackInstallType
|
||||
}
|
||||
|
||||
/**
|
||||
* Returned by `/customnode/getmappings`.
|
||||
*/
|
||||
export type ManagerMappings = Record<
|
||||
NonNullable<components['schemas']['Node']['name']>,
|
||||
[
|
||||
/** List of ComfyNode names included in the pack */
|
||||
Array<components['schemas']['ComfyNode']['comfy_node_name']>,
|
||||
{
|
||||
/** The display name of the pack */
|
||||
title_aux: string
|
||||
}
|
||||
]
|
||||
>
|
||||
|
||||
/**
|
||||
* Payload for `/manager/queue/install`
|
||||
*/
|
||||
export interface InstallPackParams extends ManagerPackInfo {
|
||||
/**
|
||||
* Semantic version, Git commit hash, `latest`, or `nightly`.
|
||||
*/
|
||||
selected_version: WorkflowNodeProperties['ver'] | SelectedVersion
|
||||
/**
|
||||
* The GitHub link to the repository of the pack to install.
|
||||
* Required if `selected_version` is `nightly`.
|
||||
*/
|
||||
repository: string
|
||||
/**
|
||||
* List of PyPi dependency names associated with the pack.
|
||||
* Used in coordination with pip package whitelist and version lock features.
|
||||
*/
|
||||
pip?: string[]
|
||||
mode: ManagerDatabaseSource
|
||||
channel: ManagerChannel
|
||||
skip_post_install?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Params for `/manager/queue/update_all`
|
||||
*/
|
||||
export interface UpdateAllPacksParams {
|
||||
mode?: ManagerDatabaseSource
|
||||
export function isMergedNodePack(
|
||||
pack: MergedNodePack | RegistryPack
|
||||
): pack is MergedNodePack {
|
||||
return 'comfy_nodes' in pack && Array.isArray(pack.comfy_nodes)
|
||||
}
|
||||
|
||||
export interface ManagerState {
|
||||
selectedTabId: ManagerTab
|
||||
searchQuery: string
|
||||
searchMode: SearchMode
|
||||
searchMode: 'nodes' | 'packs'
|
||||
sortField: string
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,22 +24,22 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should initialize with empty logs array', () => {
|
||||
const { logs } = useServerLogs()
|
||||
const { logs } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
expect(logs.value).toEqual([])
|
||||
})
|
||||
|
||||
it('should not subscribe to logs by default', () => {
|
||||
useServerLogs()
|
||||
useServerLogs({ ui_id: 'test-ui-id' })
|
||||
expect(api.subscribeLogs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should subscribe to logs when immediate is true', () => {
|
||||
useServerLogs({ immediate: true })
|
||||
useServerLogs({ ui_id: 'test-ui-id', immediate: true })
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should start listening when startListening is called', async () => {
|
||||
const { startListening } = useServerLogs()
|
||||
const { startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
|
||||
await startListening()
|
||||
|
||||
@@ -47,16 +47,21 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should stop listening when stopListening is called', async () => {
|
||||
const { startListening, stopListening } = useServerLogs()
|
||||
const { startListening, stopListening } = useServerLogs({
|
||||
ui_id: 'test-ui-id'
|
||||
})
|
||||
|
||||
await startListening()
|
||||
await stopListening()
|
||||
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(false)
|
||||
// TODO: Update this test when subscribeLogs(false) is re-enabled
|
||||
// Currently commented out in useServerLogs to prevent logs from stopping
|
||||
// after 1st of multiple queue tasks
|
||||
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should register event listener when starting', async () => {
|
||||
const { startListening } = useServerLogs()
|
||||
const { startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
|
||||
await startListening()
|
||||
|
||||
@@ -68,16 +73,30 @@ describe('useServerLogs', () => {
|
||||
})
|
||||
|
||||
it('should handle log messages correctly', async () => {
|
||||
const { logs, startListening } = useServerLogs()
|
||||
const { logs, startListening } = useServerLogs({ ui_id: 'test-ui-id' })
|
||||
|
||||
await startListening()
|
||||
|
||||
// Get the callback that was registered with useEventListener
|
||||
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
|
||||
// Get the callbacks that were registered with useEventListener
|
||||
const mockCalls = vi.mocked(useEventListener).mock.calls
|
||||
const logsCallback = mockCalls.find((call) => call[1] === 'logs')?.[2] as (
|
||||
event: CustomEvent<LogsWsMessage>
|
||||
) => void
|
||||
const taskStartedCallback = mockCalls.find(
|
||||
(call) => call[1] === 'cm-task-started'
|
||||
)?.[2] as (event: CustomEvent<any>) => void
|
||||
|
||||
// Simulate receiving a log event
|
||||
// First, simulate task started event
|
||||
const taskStartedEvent = new CustomEvent('cm-task-started', {
|
||||
detail: {
|
||||
type: 'cm-task-started',
|
||||
ui_id: 'test-ui-id'
|
||||
}
|
||||
})
|
||||
taskStartedCallback(taskStartedEvent)
|
||||
await nextTick()
|
||||
|
||||
// Now simulate receiving a log event
|
||||
const mockEvent = new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
@@ -85,7 +104,7 @@ describe('useServerLogs', () => {
|
||||
} as unknown as LogsWsMessage
|
||||
}) as CustomEvent<LogsWsMessage>
|
||||
|
||||
eventCallback(mockEvent)
|
||||
logsCallback(mockEvent)
|
||||
await nextTick()
|
||||
|
||||
expect(logs.value).toEqual(['Log message 1', 'Log message 2'])
|
||||
@@ -93,15 +112,32 @@ describe('useServerLogs', () => {
|
||||
|
||||
it('should use the message filter if provided', async () => {
|
||||
const { logs, startListening } = useServerLogs({
|
||||
ui_id: 'test-ui-id',
|
||||
messageFilter: (msg) => msg !== 'remove me'
|
||||
})
|
||||
|
||||
await startListening()
|
||||
|
||||
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
|
||||
// Get the callbacks that were registered with useEventListener
|
||||
const mockCalls = vi.mocked(useEventListener).mock.calls
|
||||
const logsCallback = mockCalls.find((call) => call[1] === 'logs')?.[2] as (
|
||||
event: CustomEvent<LogsWsMessage>
|
||||
) => void
|
||||
const taskStartedCallback = mockCalls.find(
|
||||
(call) => call[1] === 'cm-task-started'
|
||||
)?.[2] as (event: CustomEvent<any>) => void
|
||||
|
||||
// First, simulate task started event
|
||||
const taskStartedEvent = new CustomEvent('cm-task-started', {
|
||||
detail: {
|
||||
type: 'cm-task-started',
|
||||
ui_id: 'test-ui-id'
|
||||
}
|
||||
})
|
||||
taskStartedCallback(taskStartedEvent)
|
||||
await nextTick()
|
||||
|
||||
// Now simulate receiving a log event
|
||||
const mockEvent = new CustomEvent('logs', {
|
||||
detail: {
|
||||
type: 'logs',
|
||||
@@ -113,7 +149,7 @@ describe('useServerLogs', () => {
|
||||
} as unknown as LogsWsMessage
|
||||
}) as CustomEvent<LogsWsMessage>
|
||||
|
||||
eventCallback(mockEvent)
|
||||
logsCallback(mockEvent)
|
||||
await nextTick()
|
||||
|
||||
expect(logs.value).toEqual(['Log message 1 dont remove me', ''])
|
||||
|
||||
360
tests-ui/tests/composables/useUpdateAvailableNodes.test.ts
Normal file
360
tests-ui/tests/composables/useUpdateAvailableNodes.test.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
||||
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
// Import mocked utils
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
// Mock Vue's onMounted to execute immediately for testing
|
||||
vi.mock('vue', async () => {
|
||||
const actual = await vi.importActual('vue')
|
||||
return {
|
||||
...actual,
|
||||
onMounted: (cb: () => void) => cb()
|
||||
}
|
||||
})
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('@/composables/nodePack/useInstalledPacks', () => ({
|
||||
useInstalledPacks: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyManagerStore', () => ({
|
||||
useComfyManagerStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
compareVersions: vi.fn(),
|
||||
isSemVer: vi.fn()
|
||||
}))
|
||||
|
||||
const mockUseInstalledPacks = vi.mocked(useInstalledPacks)
|
||||
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
|
||||
|
||||
const mockCompareVersions = vi.mocked(compareVersions)
|
||||
const mockIsSemVer = vi.mocked(isSemVer)
|
||||
|
||||
describe('useUpdateAvailableNodes', () => {
|
||||
const mockInstalledPacks = [
|
||||
{
|
||||
id: 'pack-1',
|
||||
name: 'Outdated Pack',
|
||||
latest_version: { version: '2.0.0' }
|
||||
},
|
||||
{
|
||||
id: 'pack-2',
|
||||
name: 'Up to Date Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
},
|
||||
{
|
||||
id: 'pack-3',
|
||||
name: 'Nightly Pack',
|
||||
latest_version: { version: '1.5.0' }
|
||||
},
|
||||
{
|
||||
id: 'pack-4',
|
||||
name: 'No Latest Version',
|
||||
latest_version: null
|
||||
}
|
||||
]
|
||||
|
||||
const mockStartFetchInstalled = vi.fn()
|
||||
const mockIsPackInstalled = vi.fn()
|
||||
const mockGetInstalledPackVersion = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Default setup
|
||||
mockIsPackInstalled.mockReturnValue(true)
|
||||
mockGetInstalledPackVersion.mockImplementation((id: string) => {
|
||||
switch (id) {
|
||||
case 'pack-1':
|
||||
return '1.0.0' // outdated
|
||||
case 'pack-2':
|
||||
return '1.0.0' // up to date
|
||||
case 'pack-3':
|
||||
return 'nightly-abc123' // nightly
|
||||
case 'pack-4':
|
||||
return '1.0.0' // no latest version
|
||||
default:
|
||||
return '1.0.0'
|
||||
}
|
||||
})
|
||||
|
||||
mockIsSemVer.mockImplementation(
|
||||
(version: string): version is `${number}.${number}.${number}` => {
|
||||
return !version.includes('nightly')
|
||||
}
|
||||
)
|
||||
|
||||
mockCompareVersions.mockImplementation(
|
||||
(latest: string | undefined, installed: string | undefined) => {
|
||||
if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated
|
||||
if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date
|
||||
return 0
|
||||
}
|
||||
)
|
||||
|
||||
mockUseComfyManagerStore.mockReturnValue({
|
||||
isPackInstalled: mockIsPackInstalled,
|
||||
getInstalledPackVersion: mockGetInstalledPackVersion
|
||||
} as any)
|
||||
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
})
|
||||
|
||||
describe('core filtering logic', () => {
|
||||
it('identifies outdated packs correctly', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Should only include pack-1 (outdated)
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(1)
|
||||
expect(updateAvailableNodePacks.value[0].id).toBe('pack-1')
|
||||
})
|
||||
|
||||
it('excludes up-to-date packs', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('excludes nightly packs from updates', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('excludes packs with no latest version', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[3]]), // pack-4: no latest version
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('excludes uninstalled packs', () => {
|
||||
mockIsPackInstalled.mockReturnValue(false)
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns empty array when no installed packs exist', () => {
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
expect(updateAvailableNodePacks.value).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasUpdateAvailable computed', () => {
|
||||
it('returns true when updates are available', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1: outdated
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasUpdateAvailable } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasUpdateAvailable.value).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when no updates are available', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[1]]), // pack-2: up to date
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { hasUpdateAvailable } = useUpdateAvailableNodes()
|
||||
|
||||
expect(hasUpdateAvailable.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('automatic data fetching', () => {
|
||||
it('fetches installed packs automatically when none exist', () => {
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
expect(mockStartFetchInstalled).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not fetch when packs already exist', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
expect(mockStartFetchInstalled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not fetch when already loading', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(true),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
expect(mockStartFetchInstalled).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('state management', () => {
|
||||
it('exposes loading state from useInstalledPacks', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(true),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { isLoading } = useUpdateAvailableNodes()
|
||||
|
||||
expect(isLoading.value).toBe(true)
|
||||
})
|
||||
|
||||
it('exposes error state from useInstalledPacks', () => {
|
||||
const testError = 'Failed to fetch installed packs'
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([]),
|
||||
isLoading: ref(false),
|
||||
error: ref(testError),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { error } = useUpdateAvailableNodes()
|
||||
|
||||
expect(error.value).toBe(testError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reactivity', () => {
|
||||
it('updates when installed packs change', async () => {
|
||||
const installedPacksRef = ref([])
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: installedPacksRef,
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks, hasUpdateAvailable } =
|
||||
useUpdateAvailableNodes()
|
||||
|
||||
// Initially empty
|
||||
expect(updateAvailableNodePacks.value).toEqual([])
|
||||
expect(hasUpdateAvailable.value).toBe(false)
|
||||
|
||||
// Update installed packs
|
||||
installedPacksRef.value = [mockInstalledPacks[0]] as any // pack-1: outdated
|
||||
await nextTick()
|
||||
|
||||
// Should update available updates
|
||||
expect(updateAvailableNodePacks.value).toHaveLength(1)
|
||||
expect(hasUpdateAvailable.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('version comparison logic', () => {
|
||||
it('calls compareVersions with correct parameters', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[0]]), // pack-1
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockCompareVersions).toHaveBeenCalledWith('2.0.0', '1.0.0')
|
||||
})
|
||||
|
||||
it('calls isSemVer to check nightly versions', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockIsSemVer).toHaveBeenCalledWith('nightly-abc123')
|
||||
})
|
||||
|
||||
it('calls isPackInstalled for each pack', () => {
|
||||
mockUseInstalledPacks.mockReturnValue({
|
||||
installedPacks: ref(mockInstalledPacks),
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
startFetchInstalled: mockStartFetchInstalled
|
||||
} as any)
|
||||
|
||||
const { updateAvailableNodePacks } = useUpdateAvailableNodes()
|
||||
|
||||
// Access the computed to trigger the logic
|
||||
expect(updateAvailableNodePacks.value).toBeDefined()
|
||||
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-1')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-2')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-3')
|
||||
expect(mockIsPackInstalled).toHaveBeenCalledWith('pack-4')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -28,7 +28,7 @@ describe('useManagerQueue', () => {
|
||||
const getEventListenerCallback = () =>
|
||||
vi.mocked(api.addEventListener).mock.calls[0][1]
|
||||
|
||||
const simulateServerStatus = async (status: 'done' | 'in_progress') => {
|
||||
const simulateServerStatus = async (status: 'all-done' | 'in_progress') => {
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: { status }
|
||||
})
|
||||
@@ -49,7 +49,7 @@ describe('useManagerQueue', () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -104,7 +104,7 @@ describe('useManagerQueue', () => {
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
|
||||
it('should handle missing status property gracefully', async () => {
|
||||
@@ -119,7 +119,7 @@ describe('useManagerQueue', () => {
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('done')
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('useManagerQueue', () => {
|
||||
it('should start the next task when server is idle and queue has items', async () => {
|
||||
const { queue, mockTask } = createQueueWithMockTask()
|
||||
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Task should have been started
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
@@ -138,7 +138,7 @@ describe('useManagerQueue', () => {
|
||||
const { mockTask } = createQueueWithMockTask()
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
@@ -148,7 +148,7 @@ describe('useManagerQueue', () => {
|
||||
await simulateServerStatus('in_progress')
|
||||
expect(mockTask.onComplete).not.toHaveBeenCalled()
|
||||
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('useManagerQueue', () => {
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
@@ -167,7 +167,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Should not throw errors even without onComplete
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
@@ -184,14 +184,14 @@ describe('useManagerQueue', () => {
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
|
||||
// Process first task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Process second task
|
||||
@@ -201,7 +201,7 @@ describe('useManagerQueue', () => {
|
||||
// Complete second task
|
||||
await mockTask2.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask2.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Queue should be empty and all tasks done
|
||||
@@ -219,7 +219,7 @@ describe('useManagerQueue', () => {
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Let the promise rejection happen
|
||||
@@ -231,7 +231,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// onComplete should still be called for failed tasks
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
@@ -252,7 +252,7 @@ describe('useManagerQueue', () => {
|
||||
])
|
||||
|
||||
// Task 1
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
@@ -266,7 +266,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Task 2
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
@@ -279,7 +279,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Task 3
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
expect(mockTask3.task).toHaveBeenCalled()
|
||||
@@ -297,7 +297,7 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Add first task and start processing
|
||||
queue.enqueueTask(mockTask1)
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Add second task while first is processing
|
||||
@@ -307,7 +307,7 @@ describe('useManagerQueue', () => {
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Second task should now be processed
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
@@ -318,9 +318,9 @@ describe('useManagerQueue', () => {
|
||||
|
||||
// Cycle server status without any tasks
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('done')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Should not cause any errors
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
|
||||
@@ -4,10 +4,10 @@ import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import {
|
||||
InstalledPacksResponse,
|
||||
ManagerPackInstalled
|
||||
} from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
|
||||
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
|
||||
type ManagerPackInstalled = components['schemas']['ManagerPackInstalled']
|
||||
|
||||
vi.mock('@/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn()
|
||||
|
||||
Reference in New Issue
Block a user