Compare commits

...

2 Commits

Author SHA1 Message Date
Glary-Bot
ffad0d596e fix: remove redundant prompt shape validation in extractPrompt
The prompt field at workflow.prompt is a well-defined backend field
that always contains the API prompt. The zWorkflowContainer Zod schema
already validates the outer structure. The manual isValid check was
over-engineering with no realistic scenario where non-prompt data
would appear at that field.
2026-04-07 01:03:13 +00:00
Glary-Bot
9bfda8cfe2 fix: fall back to API prompt when opening workflow for API-dispatched jobs
API-dispatched jobs don't populate extra_pnginfo.workflow, so 'Open as
workflow in new tab' silently failed. Now falls back to loading the
API-format prompt data via loadApiJson, which auto-arranges nodes.
2026-04-07 00:39:11 +00:00
5 changed files with 116 additions and 9 deletions

View File

@@ -12,9 +12,10 @@ import { useWorkflowService } from '@/platform/workflow/core/services/workflowSe
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { ResultItem, ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { downloadBlob } from '@/scripts/utils'
import { useDialogService } from '@/services/dialogService'
import { getJobWorkflow } from '@/services/jobOutputCache'
import { getJobPrompt, getJobWorkflow } from '@/services/jobOutputCache'
import { useLitegraphService } from '@/services/litegraphService'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
@@ -59,11 +60,19 @@ export function useJobMenu(
const openJobWorkflow = async (item?: JobListItem | null) => {
const target = resolveItem(item)
if (!target) return
const data = await getJobWorkflow(target.id)
if (!data) return
const filename = `Job ${target.id}.json`
const temp = workflowStore.createTemporary(filename, data)
await workflowService.openWorkflow(temp)
const data = await getJobWorkflow(target.id)
if (data) {
const temp = workflowStore.createTemporary(filename, data)
await workflowService.openWorkflow(temp)
return
}
const prompt = await getJobPrompt(target.id)
if (prompt) {
app.loadApiJson(prompt, filename)
}
}
const copyJobId = async (item?: JobListItem | null) => {
@@ -182,7 +191,8 @@ export function useJobMenu(
const exportJobWorkflow = async () => {
const item = currentMenuItem()
if (!item) return
const data = await getJobWorkflow(item.id)
const data =
(await getJobWorkflow(item.id)) ?? (await getJobPrompt(item.id))
if (!data) return
const settingStore = useSettingStore()

View File

@@ -6,7 +6,10 @@
* All distributions use the /jobs endpoint.
*/
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import { validateComfyWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { JobId } from '@/schemas/apiSchema'
@@ -159,3 +162,19 @@ export async function extractWorkflow(
return validated ?? undefined
}
/**
* Fallback for API-dispatched workflows where extra_pnginfo.workflow
* is not populated. Extracts the API-format prompt from workflow.prompt.
*/
export function extractPrompt(
job: JobDetail | undefined
): ComfyApiWorkflow | undefined {
const parsed = zWorkflowContainer.safeParse(job?.workflow)
if (!parsed.success) return undefined
const prompt = parsed.data.prompt
if (!prompt || Object.keys(prompt).length === 0) return undefined
return prompt as ComfyApiWorkflow
}

View File

@@ -97,6 +97,7 @@ export const zJobsListResponse = z.object({
/** Schema for workflow container structure in job detail responses */
export const zWorkflowContainer = z.object({
prompt: z.record(z.string(), z.unknown()).optional(),
extra_data: z
.object({
extra_pnginfo: z

View File

@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { JobDetail } from '@/platform/remote/comfyui/jobs/jobTypes'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import {
extractPrompt,
extractWorkflow,
fetchJobDetail
} from '@/platform/remote/comfyui/jobs/fetchJobs'
@@ -125,3 +126,66 @@ describe('extractWorkflow', () => {
expect(result).toBeUndefined()
})
})
const mockApiPrompt = {
'1': {
class_type: 'SolidMask',
inputs: { value: 1, width: 512, height: 512 }
},
'2': {
class_type: 'MaskToImage',
inputs: { mask: ['1', 0] }
},
'3': {
class_type: 'SaveImage',
inputs: { images: ['2', 0], filename_prefix: 'test' }
}
}
const mockApiJobDetail: JobDetail = {
id: 'api-job-id',
status: 'completed',
create_time: 1234567890,
update_time: 1234567900,
workflow: {
prompt: mockApiPrompt,
extra_data: {}
},
outputs: {}
}
describe('extractPrompt', () => {
it('should extract prompt from API-dispatched job detail', () => {
const result = extractPrompt(mockApiJobDetail)
expect(result).toEqual(mockApiPrompt)
})
it('should return undefined when job is undefined', () => {
const result = extractPrompt(undefined)
expect(result).toBeUndefined()
})
it('should return undefined when workflow has no prompt', () => {
const jobWithoutPrompt: JobDetail = {
...mockApiJobDetail,
workflow: { extra_data: {} }
}
const result = extractPrompt(jobWithoutPrompt)
expect(result).toBeUndefined()
})
it('should return undefined when prompt is empty', () => {
const jobWithEmptyPrompt: JobDetail = {
...mockApiJobDetail,
workflow: { prompt: {}, extra_data: {} }
}
const result = extractPrompt(jobWithEmptyPrompt)
expect(result).toBeUndefined()
})
})

View File

@@ -9,8 +9,14 @@
import QuickLRU from '@alloc/quick-lru'
import type { JobDetail } from '@/platform/remote/comfyui/jobs/jobTypes'
import { extractWorkflow } from '@/platform/remote/comfyui/jobs/fetchJobs'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import {
extractPrompt,
extractWorkflow
} from '@/platform/remote/comfyui/jobs/fetchJobs'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
import type { TaskOutput } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { ResultItemImpl } from '@/stores/queueStore'
@@ -114,3 +120,10 @@ export async function getJobWorkflow(
const detail = await getJobDetail(jobId)
return await extractWorkflow(detail)
}
export async function getJobPrompt(
jobId: string
): Promise<ComfyApiWorkflow | undefined> {
const detail = await getJobDetail(jobId)
return extractPrompt(detail)
}