mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
Road to No Explicit Any Part 6: Composables and Extensions (#8083)
## Summary - Type `onExecuted` callbacks with `NodeExecutionOutput` in saveMesh.ts and uploadAudio.ts - Type composable parameters and return values properly (useLoad3dViewer, useImageMenuOptions, useJobMenu, useResultGallery, useContextMenuTranslation) - Type `taskRef` as `TaskItemImpl` with updated test mocks - Fix error catch and index signature patterns without `any` - Add `NodeOutputWith<T>` generic helper for typed access to passthrough properties on `NodeExecutionOutput` ## Test plan - [x] `pnpm typecheck` passes - [x] `pnpm lint` passes - [x] Unit tests pass for affected files - [x] Sourcegraph checks confirm no external usage of modified types ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8083-Road-to-No-Explicit-Any-Part-6-Composables-and-Extensions-2e96d73d3650810fb033d745bf88a22b) by [Unito](https://www.unito.io)
This commit is contained in:
committed by
GitHub
parent
0d5ca96a2b
commit
c56e8425d4
@@ -4,6 +4,7 @@ import { defineComponent, nextTick } from 'vue'
|
||||
|
||||
import JobGroupsList from '@/components/queue/job/JobGroupsList.vue'
|
||||
import type { JobGroup, JobListItem } from '@/composables/queue/useJobList'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
const QueueJobItemStub = defineComponent({
|
||||
name: 'QueueJobItemStub',
|
||||
@@ -25,20 +26,25 @@ const QueueJobItemStub = defineComponent({
|
||||
template: '<div class="queue-job-item-stub"></div>'
|
||||
})
|
||||
|
||||
const createJobItem = (overrides: Partial<JobListItem> = {}): JobListItem => ({
|
||||
id: 'job-id',
|
||||
title: 'Example job',
|
||||
meta: 'Meta text',
|
||||
state: 'running',
|
||||
iconName: 'icon',
|
||||
iconImageUrl: 'https://example.com/icon.png',
|
||||
showClear: true,
|
||||
taskRef: { workflow: { id: 'workflow-id' } },
|
||||
progressTotalPercent: 60,
|
||||
progressCurrentPercent: 30,
|
||||
runningNodeName: 'Node A',
|
||||
...overrides
|
||||
})
|
||||
const createJobItem = (overrides: Partial<JobListItem> = {}): JobListItem => {
|
||||
const { taskRef, ...rest } = overrides
|
||||
return {
|
||||
id: 'job-id',
|
||||
title: 'Example job',
|
||||
meta: 'Meta text',
|
||||
state: 'running',
|
||||
iconName: 'icon',
|
||||
iconImageUrl: 'https://example.com/icon.png',
|
||||
showClear: true,
|
||||
taskRef: (taskRef ?? {
|
||||
workflow: { id: 'workflow-id' }
|
||||
}) as TaskItemImpl,
|
||||
progressTotalPercent: 60,
|
||||
progressCurrentPercent: 30,
|
||||
runningNodeName: 'Node A',
|
||||
...rest
|
||||
}
|
||||
}
|
||||
|
||||
const mountComponent = (groups: JobGroup[]) =>
|
||||
mount(JobGroupsList, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
import type { MenuOption } from './useMoreOptionsMenu'
|
||||
@@ -16,7 +17,7 @@ export function useImageMenuOptions() {
|
||||
void commandStore.execute('Comfy.MaskEditor.OpenMaskEditor')
|
||||
}
|
||||
|
||||
const openImage = (node: any) => {
|
||||
const openImage = (node: LGraphNode) => {
|
||||
if (!node?.imgs?.length) return
|
||||
const img = node.imgs[node.imageIndex ?? 0]
|
||||
if (!img) return
|
||||
@@ -25,7 +26,7 @@ export function useImageMenuOptions() {
|
||||
window.open(url.toString(), '_blank')
|
||||
}
|
||||
|
||||
const copyImage = async (node: any) => {
|
||||
const copyImage = async (node: LGraphNode) => {
|
||||
if (!node?.imgs?.length) return
|
||||
const img = node.imgs[node.imageIndex ?? 0]
|
||||
if (!img) return
|
||||
@@ -62,7 +63,7 @@ export function useImageMenuOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
const saveImage = (node: any) => {
|
||||
const saveImage = (node: LGraphNode) => {
|
||||
if (!node?.imgs?.length) return
|
||||
const img = node.imgs[node.imageIndex ?? 0]
|
||||
if (!img) return
|
||||
@@ -76,7 +77,7 @@ export function useImageMenuOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
const getImageMenuOptions = (node: any): MenuOption[] => {
|
||||
const getImageMenuOptions = (node: LGraphNode): MenuOption[] => {
|
||||
if (!node?.imgs?.length) return []
|
||||
|
||||
return [
|
||||
|
||||
@@ -404,8 +404,9 @@ export function useBrushDrawing(initialSettings?: {
|
||||
device = root.device
|
||||
console.warn('✅ TypeGPU initialized! Root:', root)
|
||||
console.warn('Device info:', root.device.limits)
|
||||
} catch (error: any) {
|
||||
console.warn('Failed to initialize TypeGPU:', error.message)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.warn('Failed to initialize TypeGPU:', message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ export const useCompletionSummary = () => {
|
||||
}
|
||||
if (prev && !active) {
|
||||
const start = lastActiveStartTs.value ?? 0
|
||||
const finished = queueStore.historyTasks.filter((t: any) => {
|
||||
const ts: number | undefined = t.executionEndTimestamp
|
||||
const finished = queueStore.historyTasks.filter((t) => {
|
||||
const ts = t.executionEndTimestamp
|
||||
return typeof ts === 'number' && ts >= start
|
||||
})
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export type JobListItem = {
|
||||
iconName?: string
|
||||
iconImageUrl?: string
|
||||
showClear?: boolean
|
||||
taskRef?: any
|
||||
taskRef?: TaskItemImpl
|
||||
progressTotalPercent?: number
|
||||
progressCurrentPercent?: number
|
||||
runningNodeName?: string
|
||||
|
||||
@@ -117,13 +117,24 @@ vi.mock('@/utils/formatUtil', () => ({
|
||||
}))
|
||||
|
||||
import { useJobMenu } from '@/composables/queue/useJobMenu'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
const createJobItem = (overrides: Partial<JobListItem> = {}): JobListItem => ({
|
||||
type MockTaskRef = Record<string, unknown>
|
||||
|
||||
type TestJobListItem = Omit<JobListItem, 'taskRef'> & {
|
||||
taskRef?: MockTaskRef
|
||||
}
|
||||
|
||||
const createJobItem = (
|
||||
overrides: Partial<TestJobListItem> = {}
|
||||
): JobListItem => ({
|
||||
id: overrides.id ?? 'job-1',
|
||||
title: overrides.title ?? 'Test job',
|
||||
meta: overrides.meta ?? 'meta',
|
||||
state: overrides.state ?? 'completed',
|
||||
taskRef: overrides.taskRef,
|
||||
taskRef: overrides.taskRef as Partial<TaskItemImpl> | undefined as
|
||||
| TaskItemImpl
|
||||
| undefined,
|
||||
iconName: overrides.iconName,
|
||||
iconImageUrl: overrides.iconImageUrl,
|
||||
showClear: overrides.showClear,
|
||||
|
||||
@@ -12,7 +12,8 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
|
||||
import type {
|
||||
ExecutionErrorWsMessage,
|
||||
ResultItem,
|
||||
ResultItemType
|
||||
ResultItemType,
|
||||
TaskStatus
|
||||
} from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { downloadBlob } from '@/scripts/utils'
|
||||
@@ -82,13 +83,20 @@ export function useJobMenu(
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
const findExecutionError = (
|
||||
messages: TaskStatus['messages'] | undefined
|
||||
): ExecutionErrorWsMessage | undefined => {
|
||||
const errMessage = messages?.find((m) => m[0] === 'execution_error')
|
||||
if (errMessage && errMessage[0] === 'execution_error') {
|
||||
return errMessage[1]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const copyErrorMessage = async (item?: JobListItem | null) => {
|
||||
const target = resolveItem(item)
|
||||
if (!target) return
|
||||
const msgs = target.taskRef?.status?.messages as any[] | undefined
|
||||
const err = msgs?.find((m: any) => m?.[0] === 'execution_error')?.[1] as
|
||||
| ExecutionErrorWsMessage
|
||||
| undefined
|
||||
const err = findExecutionError(target.taskRef?.status?.messages)
|
||||
const message = err?.exception_message
|
||||
if (message) await copyToClipboard(String(message))
|
||||
}
|
||||
@@ -96,10 +104,7 @@ export function useJobMenu(
|
||||
const reportError = (item?: JobListItem | null) => {
|
||||
const target = resolveItem(item)
|
||||
if (!target) return
|
||||
const msgs = target.taskRef?.status?.messages as any[] | undefined
|
||||
const err = msgs?.find((m: any) => m?.[0] === 'execution_error')?.[1] as
|
||||
| ExecutionErrorWsMessage
|
||||
| undefined
|
||||
const err = findExecutionError(target.taskRef?.status?.messages)
|
||||
if (err) useDialogService().showExecutionErrorDialog(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
/** Minimal preview item interface for gallery filtering. */
|
||||
interface PreviewItem {
|
||||
url: string
|
||||
supportsPreview: boolean
|
||||
}
|
||||
|
||||
/** Minimal task interface for gallery preview. */
|
||||
interface TaskWithPreview<T extends PreviewItem = PreviewItem> {
|
||||
previewOutput?: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages result gallery state and activation for queue items.
|
||||
*/
|
||||
export function useResultGallery(getFilteredTasks: () => any[]) {
|
||||
export function useResultGallery<T extends PreviewItem>(
|
||||
getFilteredTasks: () => TaskWithPreview<T>[]
|
||||
) {
|
||||
const galleryActiveIndex = ref(-1)
|
||||
const galleryItems = shallowRef<ResultItemImpl[]>([])
|
||||
const galleryItems = shallowRef<T[]>([])
|
||||
|
||||
const onViewItem = (item: JobListItem) => {
|
||||
const items: ResultItemImpl[] = getFilteredTasks().flatMap((t: any) => {
|
||||
const items: T[] = getFilteredTasks().flatMap((t) => {
|
||||
const preview = t.previewOutput
|
||||
return preview && preview.supportsPreview ? [preview] : []
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@ interface CivitaiModelVersionResponse {
|
||||
model: CivitaiModel
|
||||
modelId: number
|
||||
files: CivitaiModelFile[]
|
||||
[key: string]: any
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -123,7 +123,10 @@ export const useContextMenuTranslation = () => {
|
||||
}
|
||||
|
||||
// for capture translation text of input and widget
|
||||
const extraInfo: any = options.extra || options.parentMenu?.options?.extra
|
||||
const extraInfo = (options.extra ||
|
||||
options.parentMenu?.options?.extra) as
|
||||
| { inputs?: INodeInputSlot[]; widgets?: IWidget[] }
|
||||
| undefined
|
||||
// widgets and inputs
|
||||
const matchInput = value.content?.match(reInput)
|
||||
if (matchInput) {
|
||||
|
||||
@@ -5,9 +5,13 @@ import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type {
|
||||
AnimationItem,
|
||||
BackgroundRenderModeType,
|
||||
CameraConfig,
|
||||
CameraState,
|
||||
CameraType,
|
||||
LightConfig,
|
||||
MaterialMode,
|
||||
ModelConfig,
|
||||
SceneConfig,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
@@ -271,10 +275,18 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
|
||||
const sourceCameraState = source.getCameraState()
|
||||
|
||||
const sceneConfig = node.properties['Scene Config'] as any
|
||||
const modelConfig = node.properties['Model Config'] as any
|
||||
const cameraConfig = node.properties['Camera Config'] as any
|
||||
const lightConfig = node.properties['Light Config'] as any
|
||||
const sceneConfig = node.properties['Scene Config'] as
|
||||
| SceneConfig
|
||||
| undefined
|
||||
const modelConfig = node.properties['Model Config'] as
|
||||
| ModelConfig
|
||||
| undefined
|
||||
const cameraConfig = node.properties['Camera Config'] as
|
||||
| CameraConfig
|
||||
| undefined
|
||||
const lightConfig = node.properties['Light Config'] as
|
||||
| LightConfig
|
||||
| undefined
|
||||
|
||||
isPreview.value = node.type === 'Preview3D'
|
||||
|
||||
@@ -438,7 +450,9 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
materialMode: initialState.value.materialMode
|
||||
}
|
||||
|
||||
const currentCameraConfig = nodeValue.properties['Camera Config'] as any
|
||||
const currentCameraConfig = nodeValue.properties['Camera Config'] as
|
||||
| CameraConfig
|
||||
| undefined
|
||||
nodeValue.properties['Camera Config'] = {
|
||||
...currentCameraConfig,
|
||||
state: initialState.value.cameraState
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
|
||||
import type { NodeOutputWith } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
type ImageCompareOutput = NodeOutputWith<{
|
||||
a_images?: Record<string, string>[]
|
||||
b_images?: Record<string, string>[]
|
||||
}>
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.ImageCompare',
|
||||
|
||||
@@ -14,15 +19,10 @@ useExtensionService().registerExtension({
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (output: NodeExecutionOutput) {
|
||||
node.onExecuted = function (output: ImageCompareOutput) {
|
||||
onExecuted?.call(this, output)
|
||||
|
||||
const aImages = (output as Record<string, unknown>).a_images as
|
||||
| Record<string, string>[]
|
||||
| undefined
|
||||
const bImages = (output as Record<string, unknown>).b_images as
|
||||
| Record<string, string>[]
|
||||
| undefined
|
||||
const { a_images: aImages, b_images: bImages } = output
|
||||
const rand = app.getRandParam()
|
||||
|
||||
const beforeUrl =
|
||||
|
||||
@@ -15,7 +15,11 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
|
||||
import type { NodeOutputWith } from '@/schemas/apiSchema'
|
||||
|
||||
type Load3dPreviewOutput = NodeOutputWith<{
|
||||
result?: [string?, CameraState?, string?]
|
||||
}>
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
@@ -496,13 +500,11 @@ useExtensionService().registerExtension({
|
||||
config.configure(settings)
|
||||
}
|
||||
|
||||
node.onExecuted = function (output: NodeExecutionOutput) {
|
||||
node.onExecuted = function (output: Load3dPreviewOutput) {
|
||||
onExecuted?.call(this, output)
|
||||
|
||||
const result = (output as Record<string, unknown>).result as
|
||||
| unknown[]
|
||||
| undefined
|
||||
const filePath = result?.[0] as string | undefined
|
||||
const result = output.result
|
||||
const filePath = result?.[0]
|
||||
|
||||
if (!filePath) {
|
||||
const msg = t('toastMessages.unableToGetModelFilePath')
|
||||
@@ -510,8 +512,8 @@ useExtensionService().registerExtension({
|
||||
useToastStore().addAlert(msg)
|
||||
}
|
||||
|
||||
const cameraState = result?.[1] as CameraState | undefined
|
||||
const bgImagePath = result?.[2] as string | undefined
|
||||
const cameraState = result?.[1]
|
||||
const bgImagePath = result?.[2]
|
||||
|
||||
modelWidget.value = filePath?.replaceAll('\\', '/')
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { NodeOutputWith, ResultItem } from '@/schemas/apiSchema'
|
||||
|
||||
type SaveMeshOutput = NodeOutputWith<{
|
||||
'3d'?: ResultItem[]
|
||||
}>
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
@@ -70,22 +75,26 @@ useExtensionService().registerExtension({
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (message: any) {
|
||||
onExecuted?.apply(this, arguments as any)
|
||||
node.onExecuted = function (output: SaveMeshOutput) {
|
||||
onExecuted?.call(this, output)
|
||||
|
||||
const fileInfo = message['3d'][0]
|
||||
const fileInfo = output['3d']?.[0]
|
||||
|
||||
if (!fileInfo) return
|
||||
|
||||
useLoad3d(node).waitForLoad3d((load3d) => {
|
||||
const modelWidget = node.widgets?.find((w) => w.name === 'image')
|
||||
|
||||
if (load3d && modelWidget) {
|
||||
const filePath = fileInfo['subfolder'] + '/' + fileInfo['filename']
|
||||
const filePath =
|
||||
(fileInfo.subfolder ?? '') + '/' + (fileInfo.filename ?? '')
|
||||
|
||||
modelWidget.value = filePath
|
||||
|
||||
const config = new Load3DConfiguration(load3d, node.properties)
|
||||
|
||||
config.configureForSaveMesh(fileInfo['type'], filePath)
|
||||
const loadFolder = fileInfo.type as 'input' | 'output'
|
||||
config.configureForSaveMesh(loadFolder, filePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getResourceURL,
|
||||
splitFilePath
|
||||
} from '@/renderer/extensions/vueNodes/widgets/utils/audioUtils'
|
||||
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
import { useAudioService } from '@/services/audioService'
|
||||
@@ -112,14 +113,17 @@ app.registerExtension({
|
||||
audioUIWidget.element.classList.add('empty-audio-widget')
|
||||
// Populate the audio widget UI on node execution.
|
||||
const onExecuted = node.onExecuted
|
||||
node.onExecuted = function (message: any) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onExecuted?.apply(this, arguments)
|
||||
const audios = message.audio
|
||||
if (!audios) return
|
||||
node.onExecuted = function (output: NodeExecutionOutput) {
|
||||
onExecuted?.call(this, output)
|
||||
const audios = output.audio
|
||||
if (!audios?.length) return
|
||||
const audio = audios[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
||||
getResourceURL(
|
||||
audio.subfolder ?? '',
|
||||
audio.filename ?? '',
|
||||
audio.type
|
||||
)
|
||||
)
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
@@ -139,22 +143,23 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
},
|
||||
onNodeOutputsUpdated(nodeOutputs: Record<NodeLocatorId, any>) {
|
||||
onNodeOutputsUpdated(
|
||||
nodeOutputs: Record<NodeLocatorId, NodeExecutionOutput>
|
||||
) {
|
||||
for (const [nodeLocatorId, output] of Object.entries(nodeOutputs)) {
|
||||
if ('audio' in output) {
|
||||
const node = getNodeByLocatorId(app.rootGraph, nodeLocatorId)
|
||||
if (!node) continue
|
||||
if (!output.audio?.length) continue
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
const audio = output.audio[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
||||
)
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
const node = getNodeByLocatorId(app.rootGraph, nodeLocatorId)
|
||||
if (!node) continue
|
||||
|
||||
const audioUIWidget = node.widgets?.find(
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
const audio = output.audio[0]
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder ?? '', audio.filename ?? '', audio.type)
|
||||
)
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -35,6 +35,9 @@ const zOutputs = z
|
||||
|
||||
export type NodeExecutionOutput = z.infer<typeof zOutputs>
|
||||
|
||||
export type NodeOutputWith<T extends Record<string, unknown>> =
|
||||
NodeExecutionOutput & T
|
||||
|
||||
// WS messages
|
||||
const zStatusWsMessageStatus = z.object({
|
||||
exec_info: z.object({
|
||||
|
||||
Reference in New Issue
Block a user