Files
ComfyUI_frontend/src/platform/assets/utils/outputAssetUtil.ts
Hunter 63c36d3f2f feat: display original asset names instead of hashes in assets panel (#9626)
## Problem
Output assets in the assets panel show content hashes (e.g.,
`a1b2c3d4.png`) instead of display names (e.g., `ComfyUI_00001_.png`).

## Root Cause
Cloud inference replaces `filename` with the content hash in the output
transform pipeline. The hashed filename gets stored in the jobs table's
`preview_output` JSONB. The frontend uses this hash as the display name.

## Solution
- Add `display_name` field to `AssetItem` schema and `ResultItemImpl`
- Backend (cloud PR) joins job→assets table to resolve the original name
and injects `display_name` into job responses
- Frontend prefers `display_name` over `name` **only for display text
and download filenames**
- `asset.name` remains unchanged (the hash) for URLs, drag-to-canvas,
export filters, and output key dedup

## Backwards Compatible
- OSS: `display_name` is undefined, falls back to `asset.name` (which is
already the real filename in OSS)
- Cloud pre-deploy: `display_name` absent from API, falls back
gracefully
- Old jobs with no assets: `display_name` not injected, no change

## Cloud PR
https://github.com/Comfy-Org/cloud/pull/2747



https://github.com/user-attachments/assets/8a4c9cac-4ade-4ea2-9a70-9af240a56602
2026-03-09 01:06:28 -04:00

112 lines
2.8 KiB
TypeScript

import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import {
getJobDetail,
getPreviewableOutputsFromJobDetail
} from '@/services/jobOutputCache'
import type { ResultItemImpl } from '@/stores/queueStore'
type OutputAssetMapOptions = {
jobId: string
outputs: readonly ResultItemImpl[]
createdAt?: string
executionTimeInSeconds?: number
workflow?: OutputAssetMetadata['workflow']
excludeOutputKey?: string
}
type ResolveOutputAssetItemsOptions = {
createdAt?: string
excludeOutputKey?: string
}
type OutputKeyParts = {
nodeId?: NodeId | null
subfolder?: string | null
filename?: string | null
}
function shouldLoadFullOutputs(
outputCount: OutputAssetMetadata['outputCount'],
outputsLength: number
): boolean {
return (
typeof outputCount === 'number' &&
outputCount > 1 &&
outputsLength < outputCount
)
}
export function getOutputKey({
nodeId,
subfolder,
filename
}: OutputKeyParts): string | null {
if (nodeId == null || subfolder == null || !filename) {
return null
}
return `${nodeId}-${subfolder}-${filename}`
}
function mapOutputsToAssetItems({
jobId,
outputs,
createdAt,
executionTimeInSeconds,
workflow,
excludeOutputKey
}: OutputAssetMapOptions): AssetItem[] {
const createdAtValue = createdAt ?? new Date().toISOString()
return outputs.reduce<AssetItem[]>((items, output) => {
const outputKey = getOutputKey(output)
if (!output.filename || !outputKey || outputKey === excludeOutputKey) {
return items
}
items.push({
id: `${jobId}-${outputKey}`,
name: output.filename,
display_name: output.display_name,
size: 0,
created_at: createdAtValue,
tags: ['output'],
preview_url: output.previewUrl,
user_metadata: {
jobId,
nodeId: output.nodeId,
subfolder: output.subfolder,
executionTimeInSeconds,
workflow
}
})
return items
}, [])
}
export async function resolveOutputAssetItems(
metadata: OutputAssetMetadata,
{ createdAt, excludeOutputKey }: ResolveOutputAssetItemsOptions = {}
): Promise<AssetItem[]> {
let outputsToDisplay = metadata.allOutputs ?? []
if (shouldLoadFullOutputs(metadata.outputCount, outputsToDisplay.length)) {
const jobDetail = await getJobDetail(metadata.jobId)
const previewableOutputs = getPreviewableOutputsFromJobDetail(jobDetail)
if (previewableOutputs.length) {
outputsToDisplay = previewableOutputs
}
}
return mapOutputsToAssetItems({
jobId: metadata.jobId,
outputs: outputsToDisplay,
createdAt,
executionTimeInSeconds: metadata.executionTimeInSeconds,
workflow: metadata.workflow,
excludeOutputKey
})
}