Compare commits

..

2 Commits

Author SHA1 Message Date
Robin Huang
3e1ede9a89 feat: add app:node_added telemetry event
Fires per user-initiated node add via LGraph.onNodeAdded, tagged with a
source discriminator (sidebar_drag, search_modal, paste, programmatic).
Bulk additions during workflow load are skipped — workflow_imported
already covers that population.

Source is threaded via a synchronous module-level flag set by
withNodeAddSource() at each call site, so addNodeOnGraph keeps its
existing signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:32:47 -07:00
Matt Miller
8657bff7d9 refactor(assets): read content hash via hash field, fall back to asset_hash (#12609)
## Summary

Reads the asset content hash from a new `hash` field, preferring it and
falling back to the existing `asset_hash` alias everywhere asset hashes
are consumed. This is the frontend half of converging the asset
content-hash field name onto `hash` (which the asset content-addressing
endpoints — `from-hash`, `hash/{hash}` — already use).

## Why this is safe to land now (order-independent)

- Against the current backend (emits only `asset_hash`), `hash` is
absent, so every read falls back to `asset_hash` → **byte-identical
behavior**.
- Once the backend emits `hash`, the FE uses it — which then lets the
backend drop `asset_hash` without breaking any read.

So this can merge independently of the backend; nothing is gated on it.

## What changed

- Add `hash` to the asset zod schema (`AssetItem`) and the local
`AssetRecord` type.
- Migrate all response reads of `asset_hash` → `hash ?? asset_hash`
(combo widget, widget select items, missing-media/missing-model scans,
media asset actions, preview util, metadata util, assets store).
- The `isCloud` storage-model branches (cloud = hash-as-filename, local
= name) keep their gate — only the field source changes. Removing those
conditionals is a separate behavioral change, not part of this rename.
- The `fetchAssets({ asset_hash })` request param is intentionally left
as-is; it flips to `hash` only after the backend accepts the new param
name.

## Test plan

- `vue-tsc --noEmit` — 0 errors
- `eslint` — clean
- `vitest run` on assets / missingMedia / missingModel — 775 tests pass

---------

Co-authored-by: GitHub Action <action@github.com>
2026-06-02 23:15:24 +00:00
28 changed files with 968 additions and 904 deletions

1568
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -130,7 +130,7 @@ catalog:
vite-plugin-dts: ^4.5.4
vite-plugin-html: ^3.2.2
vite-plugin-vue-devtools: ^8.0.0
vitest: ^4.1.0
vitest: ^4.0.16
vue: ^3.5.34
vue-component-type-helpers: ^3.2.1
vue-eslint-parser: ^10.4.0

View File

@@ -67,6 +67,7 @@ import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSurveyFeatureTracking } from '@/platform/surveys/useSurveyFeatureTracking'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLitegraphService } from '@/services/litegraphService'
@@ -140,10 +141,12 @@ function closeDialog() {
function addNode(nodeDef: ComfyNodeDefImpl, dragEvent?: MouseEvent) {
const followCursor = settingStore.get('Comfy.NodeSearchBoxImpl.FollowCursor')
const node = litegraphService.addNodeOnGraph(
nodeDef,
{ pos: getNewNodeLocation() },
{ ghost: useSearchBoxV2.value && followCursor, dragEvent }
const node = withNodeAddSource('search_modal', () =>
litegraphService.addNodeOnGraph(
nodeDef,
{ pos: getNewNodeLocation() },
{ ghost: useSearchBoxV2.value && followCursor, dragEvent }
)
)
if (!node) return

View File

@@ -65,6 +65,7 @@ import ModelTreeLeaf from '@/components/sidebar/tabs/modelLibrary/ModelTreeLeaf.
import Button from '@/components/ui/button/Button.vue'
import { useTreeExpansion } from '@/composables/useTreeExpansion'
import { useSettingStore } from '@/platform/settings/settingStore'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useLitegraphService } from '@/services/litegraphService'
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
import type { ComfyModelDef, ModelFolder } from '@/stores/modelStore'
@@ -155,8 +156,8 @@ const renderedRoot = computed<TreeExplorerNode<ModelOrFolder>>(() => {
if (this.leaf && model) {
const provider = modelToNodeStore.getNodeProvider(model.directory)
if (provider) {
const graphNode = useLitegraphService().addNodeOnGraph(
provider.nodeDef
const graphNode = withNodeAddSource('sidebar_drag', () =>
useLitegraphService().addNodeOnGraph(provider.nodeDef)
)
const widget = graphNode?.widgets?.find(
(widget) => widget.name === provider.key

View File

@@ -189,6 +189,7 @@ import NodeTreeFolder from '@/components/sidebar/tabs/nodeLibrary/NodeTreeFolder
import NodeTreeLeaf from '@/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue'
import Button from '@/components/ui/button/Button.vue'
import { useTreeExpansion } from '@/composables/useTreeExpansion'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useLitegraphService } from '@/services/litegraphService'
import {
DEFAULT_GROUPING_ID,
@@ -321,8 +322,11 @@ const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
}
},
handleClick(e: MouseEvent) {
if (this.leaf && this.data) {
useLitegraphService().addNodeOnGraph(this.data)
const nodeDef = this.data
if (this.leaf && nodeDef) {
withNodeAddSource('sidebar_drag', () =>
useLitegraphService().addNodeOnGraph(nodeDef)
)
} else {
toggleNodeOnEvent(e, this)
}

View File

@@ -39,6 +39,7 @@ import NodePreview from '@/components/node/NodePreview.vue'
import NodeTreeFolder from '@/components/sidebar/tabs/nodeLibrary/NodeTreeFolder.vue'
import NodeTreeLeaf from '@/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue'
import { useTreeExpansion } from '@/composables/useTreeExpansion'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useLitegraphService } from '@/services/litegraphService'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
@@ -183,8 +184,11 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
await nodeBookmarkStore.addBookmark(nodePath)
},
handleClick(e: MouseEvent) {
if (this.leaf && this.data) {
useLitegraphService().addNodeOnGraph(this.data)
const nodeDef = this.data
if (this.leaf && nodeDef) {
withNodeAddSource('sidebar_drag', () =>
useLitegraphService().addNodeOnGraph(nodeDef)
)
} else {
toggleNodeOnEvent(e, node)
}

View File

@@ -1,5 +1,6 @@
import { ref, shallowRef } from 'vue'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLitegraphService } from '@/services/litegraphService'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
@@ -37,7 +38,8 @@ function isOverCanvas(clientX: number, clientY: number): boolean {
}
function addNodeAtPosition(clientX: number, clientY: number): boolean {
if (!draggedNode.value) return false
const nodeDef = draggedNode.value
if (!nodeDef) return false
const canvas = useCanvasStore().canvas
if (!canvas) return false
if (!isOverCanvas(clientX, clientY)) return false
@@ -46,7 +48,9 @@ function addNodeAtPosition(clientX: number, clientY: number): boolean {
clientX,
clientY
} as PointerEvent)
const node = useLitegraphService().addNodeOnGraph(draggedNode.value, { pos })
const node = withNodeAddSource('sidebar_drag', () =>
useLitegraphService().addNodeOnGraph(nodeDef, { pos })
)
if (node) canvas.selectItems([node])
return true
}

View File

@@ -8,6 +8,7 @@ import { mapTaskOutputToAssetItem } from '@/platform/assets/composables/media/as
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { ResultItem, ResultItemType } from '@/schemas/apiSchema'
@@ -146,9 +147,11 @@ export function useJobMenu(
const nodeDef = nodeDefStore.nodeDefsByName[nodeType]
if (!nodeDef) return
const node = litegraphService.addNodeOnGraph(nodeDef, {
pos: litegraphService.getCanvasCenter()
})
const node = withNodeAddSource('programmatic', () =>
litegraphService.addNodeOnGraph(nodeDef, {
pos: litegraphService.getCanvasCenter()
})
)
if (!node) return

View File

@@ -4,6 +4,7 @@ import { useSharedCanvasPositionConversion } from '@/composables/element/useCanv
import { usePragmaticDroppable } from '@/composables/usePragmaticDragAndDrop'
import type { LGraphNode, Point } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { app as comfyApp } from '@/scripts/app'
@@ -37,7 +38,9 @@ export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
// Add an offset on y to make sure after adding the node, the cursor
// is on the node (top left corner)
pos[1] += LiteGraph.NODE_TITLE_HEIGHT
litegraphService.addNodeOnGraph(nodeDef, { pos })
withNodeAddSource('sidebar_drag', () =>
litegraphService.addNodeOnGraph(nodeDef, { pos })
)
} else if (node.data instanceof ComfyModelDef) {
const model = node.data
const pos = basePos
@@ -58,11 +61,8 @@ export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
if (!targetGraphNode) {
const provider = modelToNodeStore.getNodeProvider(model.directory)
if (provider) {
targetGraphNode = litegraphService.addNodeOnGraph(
provider.nodeDef,
{
pos
}
targetGraphNode = withNodeAddSource('sidebar_drag', () =>
litegraphService.addNodeOnGraph(provider.nodeDef, { pos })
)
targetProvider = provider
}

View File

@@ -6,6 +6,7 @@ import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationD
import { downloadFile } from '@/base/common/downloadUtil'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { isCloud } from '@/platform/distribution/types'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
import { useWorkflowActionsService } from '@/platform/workflow/core/services/workflowActionsService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { extractWorkflowFromAsset } from '@/platform/workflow/utils/workflowExtractionUtil'
@@ -61,7 +62,8 @@ function widgetValueVariantsForAsset(asset: AssetItem): string[] {
variants.push(`${name} [input]`)
}
}
if (asset.asset_hash) variants.push(asset.asset_hash)
const hash = asset.hash ?? asset.asset_hash
if (hash) variants.push(hash)
return variants
}
@@ -279,9 +281,11 @@ export function useMediaAssetActions() {
return
}
const node = litegraphService.addNodeOnGraph(nodeDef, {
pos: litegraphService.getCanvasCenter()
})
const node = withNodeAddSource('programmatic', () =>
litegraphService.addNodeOnGraph(nodeDef, {
pos: litegraphService.getCanvasCenter()
})
)
if (!node) {
toast.add({
@@ -296,12 +300,11 @@ export function useMediaAssetActions() {
const metadata = getOutputAssetMetadata(targetAsset.user_metadata)
const assetType = getAssetType(targetAsset, 'input')
// In Cloud mode, use asset_hash (the actual stored filename)
// In OSS mode, use the original name
const filename =
isCloud && targetAsset.asset_hash
? targetAsset.asset_hash
: targetAsset.name
// In Cloud mode, use the content hash (the actual stored filename),
// preferring hash and falling back to the deprecated asset_hash alias.
// In OSS mode, use the original name.
const cloudHash = targetAsset.hash ?? targetAsset.asset_hash
const filename = isCloud && cloudHash ? cloudHash : targetAsset.name
// Create annotated path for the asset
const annotated = createAnnotatedPath(
@@ -425,12 +428,14 @@ export function useMediaAssetActions() {
}
const center = litegraphService.getCanvasCenter()
const node = litegraphService.addNodeOnGraph(nodeDef, {
pos: [
center[0] + nodeIndex * NODE_OFFSET,
center[1] + nodeIndex * NODE_OFFSET
]
})
const node = withNodeAddSource('programmatic', () =>
litegraphService.addNodeOnGraph(nodeDef, {
pos: [
center[0] + nodeIndex * NODE_OFFSET,
center[1] + nodeIndex * NODE_OFFSET
]
})
)
if (!node) {
failed++
@@ -440,10 +445,11 @@ export function useMediaAssetActions() {
const metadata = getOutputAssetMetadata(asset.user_metadata)
const assetType = getAssetType(asset, 'input')
// In Cloud mode, use asset_hash (the actual stored filename)
// In OSS mode, use the original name
const filename =
isCloud && asset.asset_hash ? asset.asset_hash : asset.name
// In Cloud mode, use the content hash (the actual stored filename),
// preferring hash and falling back to the deprecated asset_hash alias.
// In OSS mode, use the original name.
const cloudHash = asset.hash ?? asset.asset_hash
const filename = isCloud && cloudHash ? cloudHash : asset.name
const annotated = createAnnotatedPath(
{

View File

@@ -5,6 +5,7 @@ import { z } from 'zod'
const zAsset = z.object({
id: z.string(),
name: z.string(),
hash: z.string().nullish(),
asset_hash: z.string().nullish(),
size: z.number().optional(), // TBD: Will be provided by history API in the future
mime_type: z.string().nullish(),

View File

@@ -206,5 +206,5 @@ export function getAssetCardTitle(asset: AssetItem): string {
* values or media URLs that must round-trip through the view endpoint.
*/
export function getAssetUrlFilename(asset: AssetItem): string {
return asset.asset_hash || asset.name
return asset.hash ?? asset.asset_hash ?? asset.name
}

View File

@@ -5,6 +5,7 @@ import { useAssetsStore } from '@/stores/assetsStore'
interface AssetRecord {
id: string
name: string
hash?: string
asset_hash?: string
preview_url?: string
preview_id?: string | null
@@ -42,7 +43,7 @@ export async function findOutputAsset(
name: string
): Promise<AssetRecord | undefined> {
const byHash = await fetchAssets({ asset_hash: name })
const hashMatch = byHash.find((a) => a.asset_hash === name)
const hashMatch = byHash.find((a) => (a.hash ?? a.asset_hash) === name)
if (hashMatch) return hashMatch
const byName = await fetchAssets({ name_contains: name })

View File

@@ -85,7 +85,7 @@ export function getAssetDetectionNames(
): string[] {
const names = new Set<string>()
// Treat names and hashes as opaque match keys because Cloud may use either in widget values.
addPathDetectionNames(names, asset.asset_hash, options)
addPathDetectionNames(names, asset.hash ?? asset.asset_hash, options)
addPathDetectionNames(names, asset.name, options)
const subfolder = asset.user_metadata?.subfolder

View File

@@ -501,7 +501,8 @@ function isAssetInstalled(
): boolean {
if (candidate.hash && candidate.hashType) {
const candidateHash = `${candidate.hashType}:${candidate.hash}`
if (assets.some((a) => a.asset_hash === candidateHash)) return true
if (assets.some((a) => (a.hash ?? a.asset_hash) === candidateHash))
return true
}
const normalizedName = normalizePath(candidate.name)

View File

@@ -12,6 +12,7 @@ import type {
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
NodeAddedMetadata,
NodeSearchMetadata,
NodeSearchResultMetadata,
PageViewMetadata,
@@ -198,6 +199,10 @@ export class TelemetryRegistry implements TelemetryDispatcher {
)
}
trackNodeAdded(metadata: NodeAddedMetadata): void {
this.dispatch((provider) => provider.trackNodeAdded?.(metadata))
}
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
this.dispatch((provider) => provider.trackTemplateFilterChanged?.(metadata))
}

View File

@@ -0,0 +1,81 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { ChangeTracker } from '@/scripts/changeTracker'
import { installNodeAddedTelemetry } from './installNodeAddedTelemetry'
import { withNodeAddSource } from './nodeAddSource'
const trackNodeAdded = vi.fn()
vi.mock('..', () => ({
useTelemetry: () => ({ trackNodeAdded })
}))
function fakeGraph(): LGraph {
return { onNodeAdded: undefined } as unknown as LGraph
}
function fakeNode(type: string): LGraphNode {
return { type } as unknown as LGraphNode
}
describe('installNodeAddedTelemetry', () => {
beforeEach(() => {
trackNodeAdded.mockClear()
ChangeTracker.isLoadingGraph = false
})
afterEach(() => {
ChangeTracker.isLoadingGraph = false
})
it('fires trackNodeAdded with the current source on add', () => {
const graph = fakeGraph()
installNodeAddedTelemetry(graph)
withNodeAddSource('sidebar_drag', () => {
graph.onNodeAdded?.(fakeNode('KSampler'))
})
expect(trackNodeAdded).toHaveBeenCalledExactlyOnceWith({
node_type: 'KSampler',
source: 'sidebar_drag'
})
})
it('defaults source to "unknown" outside withNodeAddSource', () => {
const graph = fakeGraph()
installNodeAddedTelemetry(graph)
graph.onNodeAdded?.(fakeNode('CheckpointLoader'))
expect(trackNodeAdded).toHaveBeenCalledWith({
node_type: 'CheckpointLoader',
source: 'unknown'
})
})
it('skips telemetry during workflow load', () => {
const graph = fakeGraph()
installNodeAddedTelemetry(graph)
ChangeTracker.isLoadingGraph = true
graph.onNodeAdded?.(fakeNode('VAEDecode'))
expect(trackNodeAdded).not.toHaveBeenCalled()
})
it('preserves an existing onNodeAdded subscriber', () => {
const graph = fakeGraph()
const previous = vi.fn()
graph.onNodeAdded = previous
installNodeAddedTelemetry(graph)
const node = fakeNode('LoadImage')
graph.onNodeAdded?.(node)
expect(previous).toHaveBeenCalledExactlyOnceWith(node)
expect(trackNodeAdded).toHaveBeenCalledOnce()
})
})

View File

@@ -0,0 +1,23 @@
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { ChangeTracker } from '@/scripts/changeTracker'
import { useTelemetry } from '..'
import { getCurrentNodeAddSource } from './nodeAddSource'
/**
* Wire `app:node_added` telemetry into a graph. Wraps any existing
* `onNodeAdded` callback so we don't displace other subscribers. Bulk
* additions during workflow load are skipped — `workflow_imported`
* already covers those.
*/
export function installNodeAddedTelemetry(graph: LGraph): void {
const previous = graph.onNodeAdded
graph.onNodeAdded = function (node) {
previous?.call(this, node)
if (ChangeTracker.isLoadingGraph) return
useTelemetry()?.trackNodeAdded({
node_type: node.type ?? 'unknown',
source: getCurrentNodeAddSource()
})
}
}

View File

@@ -0,0 +1,22 @@
import type { NodeAddSource } from '../types'
let currentSource: NodeAddSource = 'unknown'
export function getCurrentNodeAddSource(): NodeAddSource {
return currentSource
}
/**
* Set the node-add source for the duration of `fn`. Synchronous only —
* the source is read by the synchronous LGraph.onNodeAdded callback that
* fires inside `graph.add()`. Nesting restores the previous value on exit.
*/
export function withNodeAddSource<T>(source: NodeAddSource, fn: () => T): T {
const previous = currentSource
currentSource = source
try {
return fn()
} finally {
currentSource = previous
}
}

View File

@@ -10,6 +10,7 @@ import type {
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
NodeSearchMetadata,
NodeAddedMetadata,
NodeSearchResultMetadata,
PageViewMetadata,
PageVisibilityMetadata,
@@ -316,6 +317,13 @@ export class GtmTelemetryProvider implements TelemetryProvider {
})
}
trackNodeAdded(metadata: NodeAddedMetadata): void {
this.pushEvent('node_added', {
node_type: metadata.node_type,
source: metadata.source
})
}
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
this.pushEvent('template_filter', {
search_query: metadata.search_query,

View File

@@ -26,6 +26,7 @@ import type {
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
NodeSearchMetadata,
NodeAddedMetadata,
NodeSearchResultMetadata,
PageVisibilityMetadata,
RunButtonProperties,
@@ -400,6 +401,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, metadata)
}
trackNodeAdded(metadata: NodeAddedMetadata): void {
this.trackEvent(TelemetryEvents.NODE_ADDED, metadata)
}
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
this.trackEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata)
}

View File

@@ -21,6 +21,7 @@ import type {
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
NodeAddedMetadata,
NodeSearchMetadata,
NodeSearchResultMetadata,
PageViewMetadata,
@@ -457,6 +458,10 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, metadata)
}
trackNodeAdded(metadata: NodeAddedMetadata): void {
this.trackEvent(TelemetryEvents.NODE_ADDED, metadata)
}
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
this.trackEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata)
}

View File

@@ -232,6 +232,23 @@ export interface NodeSearchMetadata {
query: string
}
/**
* Node added metadata. `source` indicates how the user initiated the add.
* Bulk additions during workflow load are excluded — workflow_imported
* already covers that.
*/
export type NodeAddSource =
| 'sidebar_drag'
| 'search_modal'
| 'paste'
| 'programmatic'
| 'unknown'
export interface NodeAddedMetadata {
node_type: string
source: NodeAddSource
}
/**
* Node search result selection metadata
*/
@@ -437,6 +454,9 @@ export interface TelemetryProvider {
trackNodeSearch?(metadata: NodeSearchMetadata): void
trackNodeSearchResultSelected?(metadata: NodeSearchResultMetadata): void
// Node-added-to-canvas analytics
trackNodeAdded?(metadata: NodeAddedMetadata): void
// Template filter tracking events
trackTemplateFilterChanged?(metadata: TemplateFilterMetadata): void
@@ -523,6 +543,7 @@ export const TelemetryEvents = {
// Node Search Analytics
NODE_SEARCH: 'app:node_search',
NODE_SEARCH_RESULT_SELECTED: 'app:node_search_result_selected',
NODE_ADDED: 'app:node_added',
// Template Filter Analytics
TEMPLATE_FILTER_CHANGED: 'app:template_filter_changed',

View File

@@ -183,7 +183,7 @@ const createInputMappingWidget = (
getMediaTypeFromFilename(asset.name) ===
NODE_MEDIA_TYPE_MAP[node.comfyClass ?? '']
)
.map((asset) => asset.asset_hash)
.map((asset) => asset.hash ?? asset.asset_hash)
.filter((hash): hash is string => !!hash)
)

View File

@@ -132,7 +132,7 @@ export function useWidgetSelectItems(options: UseWidgetSelectItemsOptions) {
// already carry their own URL-resolvable filename. Expanding them via
// resolveOutputAssetItems would synthesize sibling AssetItems without
// an asset_hash and reintroduce the FE-227 hash→name fallback bug.
if (asset.asset_hash) continue
if (asset.hash ?? asset.asset_hash) continue
const meta = getOutputAssetMetadata(asset.user_metadata)
if (!meta) continue

View File

@@ -25,6 +25,7 @@ import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { installNodeAddedTelemetry } from '@/platform/telemetry/nodeAdded/installNodeAddedTelemetry'
import type { WorkflowOpenSource } from '@/platform/telemetry/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { updatePendingWarnings } from '@/platform/workflow/core/utils/pendingWarnings'
@@ -888,6 +889,7 @@ export class ComfyApp {
this.addAfterConfigureHandler(graph)
this.rootGraphInternal = graph
installNodeAddedTelemetry(graph)
this.canvas = new LGraphCanvas(canvasEl, graph)
// Make canvas states reactive so we can observe changes on them.
this.canvas.state = reactive(this.canvas.state)

View File

@@ -352,8 +352,9 @@ export const useAssetsStore = defineStore('assets', () => {
const inputAssetsByFilename = computed(() => {
const map = new Map<string, AssetItem>()
for (const asset of inputAssets.value) {
if (asset.asset_hash) {
map.set(asset.asset_hash, asset)
const hash = asset.hash ?? asset.asset_hash
if (hash) {
map.set(hash, asset)
}
}
return map

View File

@@ -4,6 +4,7 @@ import type {
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { withNodeAddSource } from '@/platform/telemetry/nodeAdded/nodeAddSource'
/**
* Serialises an array of nodes using a modified version of the old Litegraph copy (& paste) function
@@ -106,7 +107,7 @@ export function deserialiseAndCreate(data: string, canvas: LGraphCanvas): void {
node.pos[1] += graph_mouse[1] - topLeft[1]
// @ts-expect-error fixme ts strict error
graph.add(node, true)
withNodeAddSource('paste', () => graph.add(node, true))
nodes.push(node)
}