mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
[feat] Import-based API versioning architecture
- Replace metadata-based approach with import-based version selection - Add dual endpoint fetching strategy (object_info + v3/object_info) - Implement proxy-based bidirectional data synchronization - Create version-specific type definitions (v1, v1.2, v3) - Add data transformation pipeline between API versions - Update extension service for version-aware invocation This provides type-safe API versioning where extensions choose their API version through imports, ensuring compile-time safety and zero breaking changes for existing extensions.
This commit is contained in:
502
frontend-v3-compatibility-plan.md
Normal file
502
frontend-v3-compatibility-plan.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# Frontend V3 Compatibility Layer Implementation Plan (Import-Based API Versioning)
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines an **import-based API versioning** approach for implementing v3 compatibility in the ComfyUI frontend. This approach provides **type-safe API versioning** through import-based version selection, **dual endpoint data fetching**, and **proxy-based data synchronization**. Extensions choose their API version at import time, ensuring compile-time type safety and backward compatibility.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **Import-Based Version Selection**: Extensions choose API version through import statements
|
||||
2. **Type-Safe API Surfaces**: Compile-time type checking for each API version
|
||||
3. **Dual Endpoint Fetching**: Simultaneous fetching from current and v3 endpoints
|
||||
4. **Proxy-Based Synchronization**: Bidirectional data sync between API versions
|
||||
5. **Zero Breaking Changes**: Existing extensions continue to work unchanged
|
||||
6. **Gradual Migration**: Developers can adopt new versions incrementally
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### 1. Import-Based API Version Selection
|
||||
|
||||
Extensions import the API version they want to use, getting typed interfaces and guaranteed compatibility:
|
||||
|
||||
```typescript
|
||||
// Legacy extensions (unchanged)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Version-specific imports
|
||||
import { app } from '@/scripts/app/v1' // v1.x API
|
||||
import { app } from '@/scripts/app/v1_2' // v1.2 API
|
||||
import { app } from '@/scripts/app/v2' // v2.x API
|
||||
import { app } from '@/scripts/app/latest' // Latest/bleeding edge
|
||||
import { app } from '@/scripts/app' // Defaults to latest
|
||||
|
||||
// Full version-specific imports
|
||||
import { app, extensionManager, api } from '@/scripts/app/v1_2'
|
||||
|
||||
// Extensions get typed, version-specific interfaces
|
||||
app.registerExtension({
|
||||
name: 'MyExtension',
|
||||
beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app: ComfyAppV1_2) {
|
||||
// nodeData is guaranteed to be in v1.2 format
|
||||
// app methods are v1.2 compatible
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Dual Endpoint Data Fetching
|
||||
|
||||
The system fetches data from both current and future API endpoints simultaneously:
|
||||
|
||||
```typescript
|
||||
// Fetch from multiple endpoints
|
||||
const fetchNodeDefinitions = async () => {
|
||||
const [currentResponse, v3Response] = await Promise.allSettled([
|
||||
api.get('/object_info'), // Current format
|
||||
api.get('/v3/object_info') // V3 format (when available)
|
||||
])
|
||||
|
||||
// Store all formats
|
||||
return {
|
||||
canonical: mergeToCanonical(currentResponse, v3Response),
|
||||
v1: transformToV1(currentResponse),
|
||||
v1_2: transformToV1_2(currentResponse),
|
||||
v3: v3Response || transformToV3(currentResponse)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proxy-Based Data Synchronization
|
||||
|
||||
Proxies ensure that changes made through any API version stay synchronized:
|
||||
|
||||
```typescript
|
||||
// Extension modifies node data through v1.2 API
|
||||
const createV1_2NodeDefProxy = (canonicalNodeDef: ComfyNodeDefLatest) => {
|
||||
return new Proxy({}, {
|
||||
get(target, prop) {
|
||||
// Map v1.2 property access to canonical format
|
||||
if (prop === 'input') {
|
||||
return transformLatestToV1_2Input(canonicalNodeDef.inputs)
|
||||
}
|
||||
return canonicalNodeDef[mapV1_2PropToLatest(prop)]
|
||||
},
|
||||
|
||||
set(target, prop, value) {
|
||||
// Map v1.2 property changes back to canonical format
|
||||
if (prop === 'input') {
|
||||
canonicalNodeDef.inputs = transformV1_2InputToLatest(value)
|
||||
notifyDataChange(canonicalNodeDef.name, prop, value)
|
||||
return true
|
||||
}
|
||||
canonicalNodeDef[mapV1_2PropToLatest(prop)] = value
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Architecture
|
||||
|
||||
### Phase 1: API Version Infrastructure
|
||||
|
||||
**1.1 Version-Specific Entry Points**
|
||||
```typescript
|
||||
// src/scripts/app/index.ts (latest/default)
|
||||
export * from './latest'
|
||||
|
||||
// src/scripts/app/v1.ts
|
||||
export { app as default } from './adapters/v1AppAdapter'
|
||||
export { extensionManager } from './adapters/v1ExtensionAdapter'
|
||||
export { api } from './adapters/v1ApiAdapter'
|
||||
|
||||
// src/scripts/app/v1_2.ts
|
||||
export { app as default } from './adapters/v1_2AppAdapter'
|
||||
export { extensionManager } from './adapters/v1_2ExtensionAdapter'
|
||||
export { api } from './adapters/v1_2ApiAdapter'
|
||||
```
|
||||
|
||||
**1.2 Version-Specific TypeScript Interfaces**
|
||||
```typescript
|
||||
// src/types/versions/v1.ts
|
||||
export interface ComfyNodeDefV1 {
|
||||
name: string
|
||||
input?: {
|
||||
required?: Record<string, any>
|
||||
optional?: Record<string, any>
|
||||
}
|
||||
output?: string[]
|
||||
output_is_list?: boolean[]
|
||||
}
|
||||
|
||||
// src/types/versions/v1_2.ts
|
||||
export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 {
|
||||
inputs?: ComfyInputSpecV1_2[]
|
||||
metadata?: NodeMetadataV1_2
|
||||
}
|
||||
|
||||
// src/types/versions/v3.ts
|
||||
export interface ComfyNodeDefV3 {
|
||||
name: string
|
||||
schema: JsonSchema
|
||||
inputs: InputSpecV3[]
|
||||
outputs: OutputSpecV3[]
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Multi-Version Data Layer
|
||||
|
||||
**2.1 Unified Data Store**
|
||||
```typescript
|
||||
// src/stores/nodeDefStore.ts
|
||||
export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const nodeDefinitions = ref<{
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
v1: Record<string, ComfyNodeDefV1>
|
||||
v1_2: Record<string, ComfyNodeDefV1_2>
|
||||
v3: Record<string, ComfyNodeDefV3>
|
||||
}>({
|
||||
canonical: {},
|
||||
v1: {},
|
||||
v1_2: {},
|
||||
v3: {}
|
||||
})
|
||||
|
||||
const fetchNodeDefinitions = async () => {
|
||||
const [currentData, v3Data] = await Promise.allSettled([
|
||||
api.get('/object_info'),
|
||||
api.get('/v3/object_info')
|
||||
])
|
||||
|
||||
nodeDefinitions.value = transformToAllVersions(currentData, v3Data)
|
||||
}
|
||||
|
||||
// Version-specific getters with reactivity
|
||||
const getNodeDefsV1 = computed(() => nodeDefinitions.value.v1)
|
||||
const getNodeDefsV1_2 = computed(() => nodeDefinitions.value.v1_2)
|
||||
const getNodeDefsV3 = computed(() => nodeDefinitions.value.v3)
|
||||
|
||||
return {
|
||||
nodeDefinitions,
|
||||
fetchNodeDefinitions,
|
||||
getNodeDefsV1,
|
||||
getNodeDefsV1_2,
|
||||
getNodeDefsV3
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**2.2 Data Transformation Pipeline**
|
||||
```typescript
|
||||
// src/utils/versionTransforms.ts
|
||||
export class VersionTransforms {
|
||||
static transformToAllVersions(currentData: any, v3Data: any) {
|
||||
const canonical = this.createCanonicalFormat(currentData, v3Data)
|
||||
|
||||
return {
|
||||
canonical,
|
||||
v1: this.canonicalToV1(canonical),
|
||||
v1_2: this.canonicalToV1_2(canonical),
|
||||
v3: v3Data || this.canonicalToV3(canonical)
|
||||
}
|
||||
}
|
||||
|
||||
static canonicalToV1(canonical: ComfyNodeDefLatest): ComfyNodeDefV1 {
|
||||
return {
|
||||
name: canonical.name,
|
||||
input: {
|
||||
required: canonical.inputs
|
||||
?.filter(i => i.required)
|
||||
.reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>),
|
||||
optional: canonical.inputs
|
||||
?.filter(i => !i.required)
|
||||
.reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
},
|
||||
output: canonical.outputs?.map(o => o.type),
|
||||
output_is_list: canonical.outputs?.map(o => o.is_list)
|
||||
}
|
||||
}
|
||||
|
||||
static canonicalToV1_2(canonical: ComfyNodeDefLatest): ComfyNodeDefV1_2 {
|
||||
return {
|
||||
...this.canonicalToV1(canonical),
|
||||
inputs: canonical.inputs?.map(input => ({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
required: input.required,
|
||||
options: input.options
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Proxy-Based Synchronization
|
||||
|
||||
**3.1 Bidirectional Data Proxies**
|
||||
```typescript
|
||||
// src/utils/versionProxies.ts
|
||||
export class VersionProxies {
|
||||
private static canonicalStore = new Map<string, ComfyNodeDefLatest>()
|
||||
private static eventBus = new EventTarget()
|
||||
|
||||
static createV1Proxy(nodeId: string): ComfyNodeDefV1 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) throw new Error(`Node ${nodeId} not found`)
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1, {
|
||||
get(target, prop: keyof ComfyNodeDefV1) {
|
||||
return VersionProxies.transformCanonicalToV1Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1, value) {
|
||||
VersionProxies.transformV1PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static createV1_2Proxy(nodeId: string): ComfyNodeDefV1_2 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) throw new Error(`Node ${nodeId} not found`)
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1_2, {
|
||||
get(target, prop: keyof ComfyNodeDefV1_2) {
|
||||
return VersionProxies.transformCanonicalToV1_2Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1_2, value) {
|
||||
VersionProxies.transformV1_2PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private static transformCanonicalToV1Property(canonical: ComfyNodeDefLatest, prop: keyof ComfyNodeDefV1) {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
return {
|
||||
required: canonical.inputs?.filter(i => i.required).reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>),
|
||||
optional: canonical.inputs?.filter(i => !i.required).reduce((acc, input) => {
|
||||
acc[input.name] = input.spec
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
}
|
||||
case 'output':
|
||||
return canonical.outputs?.map(o => o.type)
|
||||
case 'output_is_list':
|
||||
return canonical.outputs?.map(o => o.is_list)
|
||||
default:
|
||||
return canonical[prop as keyof ComfyNodeDefLatest]
|
||||
}
|
||||
}
|
||||
|
||||
private static transformV1PropertyToCanonical(canonical: ComfyNodeDefLatest, prop: keyof ComfyNodeDefV1, value: any) {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
canonical.inputs = [
|
||||
...Object.entries(value.required || {}).map(([name, spec]) => ({
|
||||
name,
|
||||
spec,
|
||||
required: true,
|
||||
type: this.inferTypeFromSpec(spec)
|
||||
})),
|
||||
...Object.entries(value.optional || {}).map(([name, spec]) => ({
|
||||
name,
|
||||
spec,
|
||||
required: false,
|
||||
type: this.inferTypeFromSpec(spec)
|
||||
}))
|
||||
]
|
||||
break
|
||||
case 'output':
|
||||
canonical.outputs = value.map((type: string, index: number) => ({
|
||||
type,
|
||||
is_list: canonical.outputs?.[index]?.is_list || false
|
||||
}))
|
||||
break
|
||||
case 'output_is_list':
|
||||
canonical.outputs = canonical.outputs?.map((output, index) => ({
|
||||
...output,
|
||||
is_list: value[index] || false
|
||||
}))
|
||||
break
|
||||
default:
|
||||
(canonical as any)[prop] = value
|
||||
}
|
||||
}
|
||||
|
||||
private static notifyChange(nodeId: string, prop: string, value: any) {
|
||||
this.eventBus.dispatchEvent(new CustomEvent('nodedef-changed', {
|
||||
detail: { nodeId, prop, value }
|
||||
}))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Extension System Integration
|
||||
|
||||
**4.1 Version-Aware Extension Service**
|
||||
```typescript
|
||||
// src/services/extensionService.ts
|
||||
export const useExtensionService = () => {
|
||||
const extensionsByVersion = new Map<string, ComfyExtension[]>()
|
||||
|
||||
const registerExtension = (extension: ComfyExtension, apiVersion: string = 'latest') => {
|
||||
extension.apiVersion = apiVersion
|
||||
|
||||
if (!extensionsByVersion.has(apiVersion)) {
|
||||
extensionsByVersion.set(apiVersion, [])
|
||||
}
|
||||
extensionsByVersion.get(apiVersion)!.push(extension)
|
||||
}
|
||||
|
||||
const invokeExtensionsForAllVersions = async (hook: string, canonicalArgs: any[]) => {
|
||||
const promises = []
|
||||
|
||||
for (const [version, extensions] of extensionsByVersion) {
|
||||
const versionPromise = invokeExtensionsForVersion(version, hook, canonicalArgs)
|
||||
promises.push(versionPromise)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
const invokeExtensionsForVersion = async (version: string, hook: string, canonicalArgs: any[]) => {
|
||||
const extensions = extensionsByVersion.get(version) || []
|
||||
|
||||
for (const extension of extensions) {
|
||||
if (extension[hook]) {
|
||||
const transformedArgs = transformArgsForVersion(version, canonicalArgs)
|
||||
await extension[hook](...transformedArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const transformArgsForVersion = (version: string, args: any[]) => {
|
||||
return args.map(arg => {
|
||||
if (arg && typeof arg === 'object' && arg.name) {
|
||||
// This is likely a node definition
|
||||
switch (version) {
|
||||
case 'v1':
|
||||
return VersionProxies.createV1Proxy(arg.name)
|
||||
case 'v1_2':
|
||||
return VersionProxies.createV1_2Proxy(arg.name)
|
||||
case 'v3':
|
||||
return VersionProxies.createV3Proxy(arg.name)
|
||||
default:
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return arg
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
registerExtension,
|
||||
invokeExtensionsForAllVersions,
|
||||
invokeExtensionsForVersion
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4.2 Version-Specific App Adapters**
|
||||
```typescript
|
||||
// src/scripts/app/adapters/v1AppAdapter.ts
|
||||
export class V1AppAdapter {
|
||||
constructor(private canonicalApp: ComfyApp) {}
|
||||
|
||||
registerExtension(extension: ComfyExtensionV1) {
|
||||
const wrappedExtension = {
|
||||
...extension,
|
||||
apiVersion: 'v1',
|
||||
beforeRegisterNodeDef: (nodeType, nodeData, app) => {
|
||||
const v1NodeData = VersionProxies.createV1Proxy(nodeData.name)
|
||||
return extension.beforeRegisterNodeDef?.(nodeType, v1NodeData, this)
|
||||
},
|
||||
nodeCreated: (node, app) => {
|
||||
return extension.nodeCreated?.(node, this)
|
||||
}
|
||||
}
|
||||
|
||||
this.canonicalApp.registerExtension(wrappedExtension)
|
||||
}
|
||||
|
||||
// Implement other ComfyApp methods with v1 compatibility
|
||||
}
|
||||
|
||||
// src/scripts/app/adapters/v1_2AppAdapter.ts
|
||||
export class V1_2AppAdapter {
|
||||
constructor(private canonicalApp: ComfyApp) {}
|
||||
|
||||
registerExtension(extension: ComfyExtensionV1_2) {
|
||||
const wrappedExtension = {
|
||||
...extension,
|
||||
apiVersion: 'v1_2',
|
||||
beforeRegisterNodeDef: (nodeType, nodeData, app) => {
|
||||
const v1_2NodeData = VersionProxies.createV1_2Proxy(nodeData.name)
|
||||
return extension.beforeRegisterNodeDef?.(nodeType, v1_2NodeData, this)
|
||||
}
|
||||
}
|
||||
|
||||
this.canonicalApp.registerExtension(wrappedExtension)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Gradual Migration Path
|
||||
|
||||
Extensions can migrate incrementally:
|
||||
|
||||
```typescript
|
||||
// Phase 1: No changes (works with latest)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Phase 2: Explicit version (better compatibility)
|
||||
import { app } from '@/scripts/app/v1_2'
|
||||
|
||||
// Phase 3: Use newer APIs when ready
|
||||
import { app } from '@/scripts/app/latest'
|
||||
```
|
||||
|
||||
### Development Tools
|
||||
|
||||
```typescript
|
||||
// Enhanced debugging for version compatibility
|
||||
ComfyUI.debugExtensions.showVersionMatrix() // Shows which extensions use which API versions
|
||||
ComfyUI.debugExtensions.testVersionCompatibility() // Tests extension against all API versions
|
||||
ComfyUI.debugExtensions.validateDataSync() // Validates proxy synchronization
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Type Safety**: Compile-time type checking for each API version
|
||||
2. **Zero Breaking Changes**: Existing extensions work unchanged
|
||||
3. **Bidirectional Sync**: Changes through any API version stay synchronized
|
||||
4. **Future Proof**: Easy to add new API versions
|
||||
5. **Performance**: No runtime version detection overhead
|
||||
6. **Developer Experience**: Clear, typed interfaces for each version
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
- **Phase 1**: API Version Infrastructure (3-4 days)
|
||||
- **Phase 2**: Multi-Version Data Layer (2-3 days)
|
||||
- **Phase 3**: Proxy-Based Synchronization (2-3 days)
|
||||
- **Phase 4**: Extension System Integration (2-3 days)
|
||||
- **Phase 5**: Migration Tools & Documentation (1-2 days)
|
||||
|
||||
**Total Estimated Time**: 10-15 days
|
||||
|
||||
This approach provides a solid foundation for API versioning that scales with ComfyUI's growth while maintaining backward compatibility and providing a smooth migration path for extension developers.
|
||||
134
frontend-v3-compatibility-summary.md
Normal file
134
frontend-v3-compatibility-summary.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# ComfyUI Frontend V3 Compatibility - Implementation Summary
|
||||
|
||||
## Core Concept
|
||||
**Import-based API versioning** with proxy-synchronized data layers. Extensions choose their API version through imports, getting typed interfaces and guaranteed backward compatibility.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### 1. Version-Specific Imports
|
||||
```typescript
|
||||
// Legacy (unchanged)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Version-specific
|
||||
import { app } from '@/scripts/app/v1' // v1.x API
|
||||
import { app } from '@/scripts/app/v1_2' // v1.2 API
|
||||
import { app } from '@/scripts/app/v3' // v3.x API
|
||||
import { app } from '@/scripts/app/latest' // Latest
|
||||
|
||||
// Typed interfaces per version
|
||||
app.registerExtension({
|
||||
beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app: ComfyAppV1_2) {
|
||||
// nodeData guaranteed to be v1.2 format
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Dual Endpoint Data Fetching
|
||||
```typescript
|
||||
// Simultaneous fetching from multiple endpoints
|
||||
const [currentData, v3Data] = await Promise.allSettled([
|
||||
api.get('/object_info'), // Current format
|
||||
api.get('/v3/object_info') // V3 format
|
||||
])
|
||||
|
||||
// All versions stored and transformed
|
||||
nodeDefinitions.value = {
|
||||
canonical: mergeToCanonical(currentData, v3Data),
|
||||
v1: transformToV1(currentData),
|
||||
v1_2: transformToV1_2(currentData),
|
||||
v3: v3Data || transformToV3(currentData)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proxy-Based Synchronization
|
||||
```typescript
|
||||
// Bidirectional data sync through proxies
|
||||
const createV1_2Proxy = (canonical: ComfyNodeDefLatest) => {
|
||||
return new Proxy({}, {
|
||||
get(target, prop) {
|
||||
return transformCanonicalToV1_2(canonical, prop)
|
||||
},
|
||||
set(target, prop, value) {
|
||||
transformV1_2ToCanonical(canonical, prop, value)
|
||||
notifyChange(canonical.name, prop, value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Structure
|
||||
|
||||
### Phase 1: API Infrastructure (3-4 days)
|
||||
- **Entry Points**: `src/scripts/app/v1.ts`, `src/scripts/app/v1_2.ts`, etc.
|
||||
- **Type Definitions**: `src/types/versions/` with version-specific interfaces
|
||||
- **Adapters**: `src/scripts/app/adapters/` for API compatibility layers
|
||||
|
||||
### Phase 2: Data Layer (2-3 days)
|
||||
- **Multi-Version Store**: Single store with all format versions
|
||||
- **Transform Pipeline**: `src/utils/versionTransforms.ts` for format conversion
|
||||
- **Reactive Getters**: Version-specific computed properties
|
||||
|
||||
### Phase 3: Proxy System (2-3 days)
|
||||
- **Bidirectional Proxies**: `src/utils/versionProxies.ts`
|
||||
- **Change Notification**: Event system for data sync
|
||||
- **Type Safety**: Proper typing for proxy objects
|
||||
|
||||
### Phase 4: Extension Integration (2-3 days)
|
||||
- **Version-Aware Service**: Extensions grouped by API version
|
||||
- **Hook Invocation**: Transform args per version before calling
|
||||
- **App Adapters**: Version-specific app instances
|
||||
|
||||
## Key Benefits
|
||||
|
||||
- **Type Safety**: Compile-time checking for each API version
|
||||
- **Zero Breaking Changes**: Existing extensions work unchanged
|
||||
- **Bidirectional Sync**: Changes through any API stay synchronized
|
||||
- **Performance**: No runtime version detection overhead
|
||||
- **Future Proof**: Easy to add new API versions
|
||||
- **Developer Experience**: Clear, typed interfaces
|
||||
|
||||
## Migration Path
|
||||
|
||||
```typescript
|
||||
// Step 1: No changes (works with latest)
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
// Step 2: Explicit version (better compatibility)
|
||||
import { app } from '@/scripts/app/v1_2'
|
||||
|
||||
// Step 3: Use newer APIs when ready
|
||||
import { app } from '@/scripts/app/latest'
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
```typescript
|
||||
// v1.2 Extension
|
||||
import { app } from '@/scripts/app/v1_2'
|
||||
|
||||
app.registerExtension({
|
||||
name: 'MyExtension',
|
||||
beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app) {
|
||||
// nodeData.input.required - v1 format
|
||||
// nodeData.inputs - v1.2 format
|
||||
if (nodeData.inputs) {
|
||||
// Use v1.2 features
|
||||
} else {
|
||||
// Fallback to v1 format
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Implementation Timeline
|
||||
- **Phase 1**: API Version Infrastructure (3-4 days)
|
||||
- **Phase 2**: Multi-Version Data Layer (2-3 days)
|
||||
- **Phase 3**: Proxy-Based Synchronization (2-3 days)
|
||||
- **Phase 4**: Extension System Integration (2-3 days)
|
||||
- **Phase 5**: Migration Tools & Documentation (1-2 days)
|
||||
|
||||
**Total**: 10-15 days
|
||||
|
||||
This approach provides type-safe API versioning that scales with ComfyUI's growth while maintaining complete backward compatibility and smooth migration paths for extension developers.
|
||||
@@ -9,6 +9,7 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import { VersionProxies } from '@/utils/versionProxies'
|
||||
|
||||
export const useExtensionService = () => {
|
||||
const extensionStore = useExtensionStore()
|
||||
@@ -128,10 +129,132 @@ export const useExtensionService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register extension with API version tracking
|
||||
*/
|
||||
const registerExtensionWithVersion = (
|
||||
extension: ComfyExtension,
|
||||
apiVersion: string = 'latest'
|
||||
) => {
|
||||
extension.apiVersion = apiVersion
|
||||
registerExtension(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke extensions for a specific API version with transformed args
|
||||
*/
|
||||
const invokeExtensionsForVersion = async <T extends any[]>(
|
||||
hook: keyof ComfyExtension,
|
||||
apiVersion: string,
|
||||
...args: T
|
||||
) => {
|
||||
const extensions = extensionStore.extensions.filter(
|
||||
(ext) =>
|
||||
ext.apiVersion === apiVersion ||
|
||||
(!ext.apiVersion && apiVersion === 'latest')
|
||||
)
|
||||
|
||||
for (const extension of extensions) {
|
||||
if (extension[hook] && typeof extension[hook] === 'function') {
|
||||
try {
|
||||
const transformedArgs = transformArgsForVersion(apiVersion, args)
|
||||
await (extension[hook] as (...args: T) => void | Promise<void>)(
|
||||
...transformedArgs
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error in extension ${extension.name} hook ${String(hook)}:`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke extensions for all API versions with appropriate data transformation
|
||||
*/
|
||||
const invokeExtensionsForAllVersions = async <T extends any[]>(
|
||||
hook: keyof ComfyExtension,
|
||||
...args: T
|
||||
) => {
|
||||
const apiVersions = new Set(['latest', 'v1', 'v1_2', 'v3'])
|
||||
const promises = []
|
||||
|
||||
for (const version of apiVersions) {
|
||||
promises.push(invokeExtensionsForVersion(hook, version, ...args))
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform arguments for specific API version
|
||||
*/
|
||||
const transformArgsForVersion = (version: string, args: any[]) => {
|
||||
return args.map((arg) => {
|
||||
// If argument looks like a node definition, create appropriate proxy
|
||||
if (
|
||||
arg &&
|
||||
typeof arg === 'object' &&
|
||||
arg.name &&
|
||||
typeof arg.name === 'string'
|
||||
) {
|
||||
try {
|
||||
switch (version) {
|
||||
case 'v1':
|
||||
return VersionProxies.createV1Proxy(arg.name)
|
||||
case 'v1_2':
|
||||
return VersionProxies.createV1_2Proxy(arg.name)
|
||||
case 'v3':
|
||||
return VersionProxies.createV3Proxy(arg.name)
|
||||
default:
|
||||
return arg
|
||||
}
|
||||
} catch (error) {
|
||||
// If proxy creation fails, return original arg
|
||||
console.warn(`Failed to create proxy for node ${arg.name}:`, error)
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return arg
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extension compatibility report by API version
|
||||
*/
|
||||
const getExtensionVersionReport = () => {
|
||||
const extensions = extensionStore.extensions
|
||||
const versionGroups = extensions.reduce(
|
||||
(acc, ext) => {
|
||||
const version = ext.apiVersion || 'latest'
|
||||
if (!acc[version]) acc[version] = []
|
||||
acc[version].push(ext)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ComfyExtension[]>
|
||||
)
|
||||
|
||||
return {
|
||||
total: extensions.length,
|
||||
versionGroups,
|
||||
details: Object.entries(versionGroups).map(([version, exts]) => ({
|
||||
version,
|
||||
count: exts.length,
|
||||
extensions: exts.map((ext) => ext.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadExtensions,
|
||||
registerExtension,
|
||||
registerExtensionWithVersion,
|
||||
invokeExtensions,
|
||||
invokeExtensionsAsync
|
||||
invokeExtensionsAsync,
|
||||
invokeExtensionsForVersion,
|
||||
invokeExtensionsForAllVersions,
|
||||
getExtensionVersionReport
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ export interface ComfyExtension {
|
||||
* The name of the extension
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* API version this extension is using (set automatically by import)
|
||||
*/
|
||||
apiVersion?: string
|
||||
/**
|
||||
* The commands defined by the extension
|
||||
*/
|
||||
|
||||
4
src/types/versions/index.ts
Normal file
4
src/types/versions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Re-export all version-specific types
|
||||
export * from './v1'
|
||||
export * from './v1_2'
|
||||
export * from './v3'
|
||||
101
src/types/versions/v1.ts
Normal file
101
src/types/versions/v1.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* V1 API node definition interface
|
||||
* This represents the current/legacy node definition format
|
||||
*/
|
||||
export interface ComfyNodeDefV1 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
input?: {
|
||||
required?: Record<string, any>
|
||||
optional?: Record<string, any>
|
||||
hidden?: Record<string, any>
|
||||
}
|
||||
output?: string[]
|
||||
output_is_list?: boolean[]
|
||||
output_name?: string[]
|
||||
output_tooltips?: string[]
|
||||
python_module?: string
|
||||
deprecated?: boolean
|
||||
experimental?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 API extension interface
|
||||
*/
|
||||
export interface ComfyExtensionV1 {
|
||||
name: string
|
||||
apiVersion?: 'v1'
|
||||
|
||||
// Lifecycle hooks
|
||||
init?(): void | Promise<void>
|
||||
setup?(): void | Promise<void>
|
||||
|
||||
// Node lifecycle hooks
|
||||
beforeRegisterNodeDef?(
|
||||
nodeType: typeof LGraphNode,
|
||||
nodeData: ComfyNodeDefV1,
|
||||
app: ComfyAppV1
|
||||
): void
|
||||
nodeCreated?(node: LGraphNode, app: ComfyAppV1): void
|
||||
|
||||
// Graph hooks
|
||||
beforeConfigureGraph?(graphData: any, missingNodeTypes: any[]): void
|
||||
afterConfigureGraph?(missingNodeTypes: any[]): void
|
||||
|
||||
// Canvas hooks
|
||||
getCustomWidgets?(): Record<string, any>
|
||||
|
||||
// Menu hooks
|
||||
addCustomNodeDefs?(defs: ComfyNodeDefV1[]): ComfyNodeDefV1[]
|
||||
|
||||
// Settings
|
||||
settings?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
defaultValue: any
|
||||
tooltip?: string
|
||||
}>
|
||||
|
||||
// Commands
|
||||
commands?: Array<{
|
||||
id: string
|
||||
function: () => void | Promise<void>
|
||||
}>
|
||||
|
||||
// Keybindings
|
||||
keybindings?: Array<{
|
||||
combo: { key: string; ctrl?: boolean; shift?: boolean; alt?: boolean }
|
||||
commandId: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 API ComfyApp interface
|
||||
* Provides the same interface as the current app
|
||||
*/
|
||||
export interface ComfyAppV1 {
|
||||
registerExtension(extension: ComfyExtensionV1): void
|
||||
|
||||
// Graph management
|
||||
loadGraphData(graphData: any): Promise<void>
|
||||
clean(): void
|
||||
|
||||
// Node management
|
||||
getNodeById(id: number): LGraphNode | null
|
||||
|
||||
// Workflow operations
|
||||
queuePrompt(number: number, batchCount?: number): Promise<void>
|
||||
|
||||
// Canvas operations
|
||||
canvas: any
|
||||
graph: any
|
||||
|
||||
// UI state
|
||||
ui: any
|
||||
}
|
||||
111
src/types/versions/v1_2.ts
Normal file
111
src/types/versions/v1_2.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { ComfyAppV1, ComfyExtensionV1, ComfyNodeDefV1 } from './v1'
|
||||
|
||||
/**
|
||||
* V1.2 API node definition interface
|
||||
* Extends V1 with additional structured input information
|
||||
*/
|
||||
export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 {
|
||||
inputs?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options?: any
|
||||
spec?: any
|
||||
tooltip?: string
|
||||
default?: any
|
||||
}>
|
||||
|
||||
metadata?: {
|
||||
version?: string
|
||||
author?: string
|
||||
description?: string
|
||||
tags?: string[]
|
||||
documentation?: string
|
||||
}
|
||||
|
||||
// Enhanced output information
|
||||
outputs?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
is_list: boolean
|
||||
tooltip?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V1.2 API extension interface
|
||||
* Extends V1 with additional capabilities
|
||||
*/
|
||||
export interface ComfyExtensionV1_2 extends ComfyExtensionV1 {
|
||||
apiVersion?: 'v1_2'
|
||||
|
||||
// V1.2 specific hooks
|
||||
beforeRegisterNodeDef?(
|
||||
nodeType: any,
|
||||
nodeData: ComfyNodeDefV1_2,
|
||||
app: ComfyAppV1_2
|
||||
): void
|
||||
nodeCreated?(node: any, app: ComfyAppV1_2): void
|
||||
|
||||
// Enhanced settings with validation
|
||||
settings?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
defaultValue: any
|
||||
tooltip?: string
|
||||
validation?: (value: any) => boolean | string
|
||||
category?: string
|
||||
options?: any[]
|
||||
}>
|
||||
|
||||
// Enhanced commands with descriptions
|
||||
commands?: Array<{
|
||||
id: string
|
||||
function: () => void | Promise<void>
|
||||
label?: string
|
||||
tooltip?: string
|
||||
category?: string
|
||||
icon?: string
|
||||
}>
|
||||
|
||||
// Bottom panel tabs
|
||||
bottomPanelTabs?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
type: 'vue' | 'custom'
|
||||
component?: any
|
||||
tooltip?: string
|
||||
}>
|
||||
|
||||
// Menu items
|
||||
menuItems?: Array<{
|
||||
path: string[]
|
||||
commands: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V1.2 API ComfyApp interface
|
||||
* Extends V1 with additional functionality
|
||||
*/
|
||||
export interface ComfyAppV1_2 extends ComfyAppV1 {
|
||||
registerExtension(extension: ComfyExtensionV1_2): void
|
||||
|
||||
// Enhanced node operations
|
||||
createNode(type: string, title?: string, options?: any): any
|
||||
removeNode(node: any): void
|
||||
|
||||
// Settings operations
|
||||
getSetting(id: string): any
|
||||
setSetting(id: string, value: any): void
|
||||
|
||||
// Command operations
|
||||
executeCommand(id: string): Promise<void>
|
||||
|
||||
// Enhanced UI access
|
||||
bottomPanel: any
|
||||
sidebar: any
|
||||
menu: any
|
||||
}
|
||||
288
src/types/versions/v3.ts
Normal file
288
src/types/versions/v3.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* V3 API node definition interface
|
||||
* Future schema-based node definition format
|
||||
*/
|
||||
export interface ComfyNodeDefV3 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
|
||||
// JSON Schema-based definition
|
||||
schema: {
|
||||
type: 'object'
|
||||
properties: Record<string, any>
|
||||
required?: string[]
|
||||
additionalProperties?: boolean
|
||||
}
|
||||
|
||||
// Structured input/output definitions
|
||||
inputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
schema: any
|
||||
validation?: any
|
||||
tooltip?: string
|
||||
default?: any
|
||||
ui?: {
|
||||
widget?: string
|
||||
options?: any
|
||||
}
|
||||
}>
|
||||
|
||||
outputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
is_list: boolean
|
||||
schema?: any
|
||||
tooltip?: string
|
||||
}>
|
||||
|
||||
// Enhanced metadata
|
||||
metadata: {
|
||||
version: string
|
||||
author: string
|
||||
description: string
|
||||
tags: string[]
|
||||
documentation?: string
|
||||
repository?: string
|
||||
license?: string
|
||||
dependencies?: Array<{
|
||||
name: string
|
||||
version?: string
|
||||
optional?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
// Execution information
|
||||
execution?: {
|
||||
async?: boolean
|
||||
gpu_memory?: number
|
||||
cpu_cores?: number
|
||||
timeout?: number
|
||||
retries?: number
|
||||
}
|
||||
|
||||
python_module?: string
|
||||
deprecated?: boolean
|
||||
experimental?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* V3 API extension interface
|
||||
* Future extension format with enhanced capabilities
|
||||
*/
|
||||
export interface ComfyExtensionV3 {
|
||||
name: string
|
||||
apiVersion: 'v3'
|
||||
|
||||
// Required metadata
|
||||
metadata: {
|
||||
version: string
|
||||
author: string
|
||||
description: string
|
||||
repository?: string
|
||||
license?: string
|
||||
dependencies?: Array<{
|
||||
name: string
|
||||
version?: string
|
||||
optional?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
// Lifecycle hooks with enhanced data
|
||||
init?(): void | Promise<void>
|
||||
setup?(): void | Promise<void>
|
||||
beforeShutdown?(): void | Promise<void>
|
||||
|
||||
// Node lifecycle hooks
|
||||
beforeRegisterNodeDef?(
|
||||
nodeType: any,
|
||||
nodeData: ComfyNodeDefV3,
|
||||
app: ComfyAppV3
|
||||
): void | Promise<void>
|
||||
afterRegisterNodeDef?(
|
||||
nodeType: any,
|
||||
nodeData: ComfyNodeDefV3,
|
||||
app: ComfyAppV3
|
||||
): void | Promise<void>
|
||||
nodeCreated?(node: any, app: ComfyAppV3): void | Promise<void>
|
||||
nodeRemoved?(node: any, app: ComfyAppV3): void | Promise<void>
|
||||
|
||||
// Graph hooks
|
||||
beforeConfigureGraph?(
|
||||
graphData: any,
|
||||
missingNodeTypes: any[]
|
||||
): void | Promise<void>
|
||||
afterConfigureGraph?(missingNodeTypes: any[]): void | Promise<void>
|
||||
graphChanged?(graph: any): void | Promise<void>
|
||||
|
||||
// Enhanced settings with full schema validation
|
||||
settings?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
schema: any
|
||||
defaultValue: any
|
||||
tooltip?: string
|
||||
category?: string
|
||||
validation?: (value: any) => boolean | string
|
||||
ui?: {
|
||||
widget?: string
|
||||
options?: any
|
||||
}
|
||||
}>
|
||||
|
||||
// Enhanced commands with full metadata
|
||||
commands?: Array<{
|
||||
id: string
|
||||
function: () => void | Promise<void>
|
||||
metadata: {
|
||||
label: string
|
||||
tooltip?: string
|
||||
category?: string
|
||||
icon?: string
|
||||
shortcut?: string
|
||||
}
|
||||
validation?: () => boolean
|
||||
}>
|
||||
|
||||
// Keybindings with enhanced options
|
||||
keybindings?: Array<{
|
||||
combo: {
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
shift?: boolean
|
||||
alt?: boolean
|
||||
meta?: boolean
|
||||
}
|
||||
commandId: string
|
||||
when?: string // Context condition
|
||||
priority?: number
|
||||
}>
|
||||
|
||||
// UI extensions
|
||||
uiExtensions?: {
|
||||
bottomPanelTabs?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
component: any
|
||||
tooltip?: string
|
||||
when?: string
|
||||
}>
|
||||
|
||||
contextMenus?: Array<{
|
||||
id: string
|
||||
items: Array<{
|
||||
id: string
|
||||
label: string
|
||||
commandId: string
|
||||
when?: string
|
||||
separator?: boolean
|
||||
}>
|
||||
}>
|
||||
|
||||
toolbars?: Array<{
|
||||
id: string
|
||||
location: 'top' | 'bottom' | 'left' | 'right'
|
||||
items: Array<{
|
||||
id: string
|
||||
type: 'button' | 'separator' | 'dropdown'
|
||||
commandId?: string
|
||||
label?: string
|
||||
icon?: string
|
||||
tooltip?: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
// API endpoints (for extensions that provide their own APIs)
|
||||
apiEndpoints?: Array<{
|
||||
path: string
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||
handler: (req: any, res: any) => void | Promise<void>
|
||||
schema?: any
|
||||
auth?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* V3 API ComfyApp interface
|
||||
* Future app interface with enhanced capabilities
|
||||
*/
|
||||
export interface ComfyAppV3 {
|
||||
registerExtension(extension: ComfyExtensionV3): Promise<void>
|
||||
|
||||
// Enhanced graph management
|
||||
graph: {
|
||||
load(data: any): Promise<void>
|
||||
save(): any
|
||||
clear(): void
|
||||
validate(): Promise<boolean>
|
||||
getNodes(): any[]
|
||||
getNodeById(id: string): any | null
|
||||
addNode(type: string, options?: any): Promise<any>
|
||||
removeNode(node: any): Promise<void>
|
||||
connectNodes(source: any, target: any, options?: any): Promise<void>
|
||||
disconnectNodes(source: any, target: any): Promise<void>
|
||||
}
|
||||
|
||||
// Enhanced workflow operations
|
||||
workflow: {
|
||||
queue(batchCount?: number): Promise<string>
|
||||
cancel(executionId?: string): Promise<void>
|
||||
pause(): Promise<void>
|
||||
resume(): Promise<void>
|
||||
getStatus(): any
|
||||
getHistory(): any[]
|
||||
getQueue(): any[]
|
||||
}
|
||||
|
||||
// Settings management
|
||||
settings: {
|
||||
get<T = any>(id: string): T
|
||||
set(id: string, value: any): Promise<void>
|
||||
getAll(): Record<string, any>
|
||||
reset(id?: string): Promise<void>
|
||||
export(): any
|
||||
import(data: any): Promise<void>
|
||||
}
|
||||
|
||||
// Command system
|
||||
commands: {
|
||||
execute(id: string, args?: any): Promise<any>
|
||||
register(command: any): void
|
||||
unregister(id: string): void
|
||||
getAll(): any[]
|
||||
isAvailable(id: string): boolean
|
||||
}
|
||||
|
||||
// UI management
|
||||
ui: {
|
||||
bottomPanel: any
|
||||
sidebar: any
|
||||
menu: any
|
||||
canvas: any
|
||||
dialogs: any
|
||||
notifications: any
|
||||
}
|
||||
|
||||
// Event system
|
||||
events: {
|
||||
on<T = unknown>(event: string, handler: (data: T) => void): void
|
||||
off<T = unknown>(event: string, handler?: (data: T) => void): void
|
||||
emit<T = unknown>(event: string, data?: T): void
|
||||
once<T = unknown>(event: string, handler: (data: T) => void): void
|
||||
}
|
||||
|
||||
// API access
|
||||
api: {
|
||||
get(path: string, options?: any): Promise<any>
|
||||
post(path: string, data?: any, options?: any): Promise<any>
|
||||
put(path: string, data?: any, options?: any): Promise<any>
|
||||
delete(path: string, options?: any): Promise<any>
|
||||
patch(path: string, data?: any, options?: any): Promise<any>
|
||||
}
|
||||
}
|
||||
519
src/utils/versionProxies.ts
Normal file
519
src/utils/versionProxies.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
import type {
|
||||
ComfyNodeDefLatest,
|
||||
ComfyNodeDefV1,
|
||||
ComfyNodeDefV1_2,
|
||||
ComfyNodeDefV3
|
||||
} from './versionTransforms'
|
||||
|
||||
/**
|
||||
* Proxy-based system for synchronizing data between different API versions
|
||||
*/
|
||||
export class VersionProxies {
|
||||
private static canonicalStore = new Map<string, ComfyNodeDefLatest>()
|
||||
private static eventBus = new EventTarget()
|
||||
|
||||
/**
|
||||
* Register a canonical node definition
|
||||
*/
|
||||
static registerCanonicalNode(nodeId: string, nodeData: ComfyNodeDefLatest) {
|
||||
this.canonicalStore.set(nodeId, nodeData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canonical node data
|
||||
*/
|
||||
static getCanonicalNode(nodeId: string): ComfyNodeDefLatest | undefined {
|
||||
return this.canonicalStore.get(nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a V1 proxy for a node
|
||||
*/
|
||||
static createV1Proxy(nodeId: string): ComfyNodeDefV1 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) {
|
||||
throw new Error(`Node ${nodeId} not found in canonical store`)
|
||||
}
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1, {
|
||||
get(target, prop: keyof ComfyNodeDefV1) {
|
||||
return VersionProxies.transformCanonicalToV1Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1, value) {
|
||||
VersionProxies.transformV1PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
},
|
||||
|
||||
has(target, prop: keyof ComfyNodeDefV1) {
|
||||
return (
|
||||
prop in canonical ||
|
||||
prop === 'input' ||
|
||||
prop === 'output' ||
|
||||
prop === 'output_is_list'
|
||||
)
|
||||
},
|
||||
|
||||
ownKeys(target) {
|
||||
return [
|
||||
'name',
|
||||
'display_name',
|
||||
'description',
|
||||
'category',
|
||||
'output_node',
|
||||
'input',
|
||||
'output',
|
||||
'output_is_list',
|
||||
'python_module'
|
||||
]
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: this.get!(target, prop, target)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a V1.2 proxy for a node
|
||||
*/
|
||||
static createV1_2Proxy(nodeId: string): ComfyNodeDefV1_2 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) {
|
||||
throw new Error(`Node ${nodeId} not found in canonical store`)
|
||||
}
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV1_2, {
|
||||
get(target, prop: keyof ComfyNodeDefV1_2) {
|
||||
return VersionProxies.transformCanonicalToV1_2Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV1_2, value) {
|
||||
VersionProxies.transformV1_2PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
},
|
||||
|
||||
has(target, prop: keyof ComfyNodeDefV1_2) {
|
||||
return (
|
||||
prop in canonical ||
|
||||
prop === 'input' ||
|
||||
prop === 'output' ||
|
||||
prop === 'output_is_list' ||
|
||||
prop === 'inputs' ||
|
||||
prop === 'metadata'
|
||||
)
|
||||
},
|
||||
|
||||
ownKeys(target) {
|
||||
return [
|
||||
'name',
|
||||
'display_name',
|
||||
'description',
|
||||
'category',
|
||||
'output_node',
|
||||
'input',
|
||||
'output',
|
||||
'output_is_list',
|
||||
'inputs',
|
||||
'metadata',
|
||||
'python_module'
|
||||
]
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: this.get!(target, prop, target)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a V3 proxy for a node
|
||||
*/
|
||||
static createV3Proxy(nodeId: string): ComfyNodeDefV3 {
|
||||
const canonical = this.canonicalStore.get(nodeId)
|
||||
if (!canonical) {
|
||||
throw new Error(`Node ${nodeId} not found in canonical store`)
|
||||
}
|
||||
|
||||
return new Proxy({} as ComfyNodeDefV3, {
|
||||
get(target, prop: keyof ComfyNodeDefV3) {
|
||||
return VersionProxies.transformCanonicalToV3Property(canonical, prop)
|
||||
},
|
||||
|
||||
set(target, prop: keyof ComfyNodeDefV3, value) {
|
||||
VersionProxies.transformV3PropertyToCanonical(canonical, prop, value)
|
||||
VersionProxies.notifyChange(nodeId, prop, value)
|
||||
return true
|
||||
},
|
||||
|
||||
has(target, prop: keyof ComfyNodeDefV3) {
|
||||
return (
|
||||
prop in canonical ||
|
||||
prop === 'schema' ||
|
||||
prop === 'inputs' ||
|
||||
prop === 'outputs'
|
||||
)
|
||||
},
|
||||
|
||||
ownKeys(target) {
|
||||
return [
|
||||
'name',
|
||||
'display_name',
|
||||
'description',
|
||||
'category',
|
||||
'output_node',
|
||||
'schema',
|
||||
'inputs',
|
||||
'outputs',
|
||||
'python_module'
|
||||
]
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: this.get!(target, prop, target)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical property to V1 format
|
||||
*/
|
||||
private static transformCanonicalToV1Property(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1
|
||||
): any {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
return canonical.input
|
||||
? {
|
||||
required: canonical.input.required,
|
||||
optional: canonical.input.optional
|
||||
}
|
||||
: undefined
|
||||
case 'output':
|
||||
return canonical.output
|
||||
case 'output_is_list':
|
||||
return canonical.output_is_list
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
return canonical[prop]
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical property to V1.2 format
|
||||
*/
|
||||
private static transformCanonicalToV1_2Property(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1_2
|
||||
): any {
|
||||
switch (prop) {
|
||||
case 'inputs':
|
||||
if (!canonical.input) return undefined
|
||||
return [
|
||||
...Object.entries(canonical.input.required || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: true,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
),
|
||||
...Object.entries(canonical.input.optional || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: false,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
)
|
||||
]
|
||||
case 'metadata':
|
||||
return {
|
||||
version: canonical.api_version,
|
||||
author: 'Unknown',
|
||||
description: canonical.description
|
||||
}
|
||||
default:
|
||||
return this.transformCanonicalToV1Property(
|
||||
canonical,
|
||||
prop as keyof ComfyNodeDefV1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical property to V3 format
|
||||
*/
|
||||
private static transformCanonicalToV3Property(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV3
|
||||
): any {
|
||||
switch (prop) {
|
||||
case 'schema': {
|
||||
const requiredInputs = Object.keys(canonical.input?.required || {})
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(canonical.input?.required || {}),
|
||||
...(canonical.input?.optional || {})
|
||||
},
|
||||
required: requiredInputs
|
||||
}
|
||||
}
|
||||
case 'inputs': {
|
||||
if (!canonical.input) return []
|
||||
const requiredInputs2 = Object.entries(canonical.input.required || {})
|
||||
const optionalInputs = Object.entries(canonical.input.optional || {})
|
||||
return [
|
||||
...requiredInputs2.map(([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: true,
|
||||
schema: spec
|
||||
})),
|
||||
...optionalInputs.map(([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: false,
|
||||
schema: spec
|
||||
}))
|
||||
]
|
||||
}
|
||||
case 'outputs':
|
||||
return (
|
||||
canonical.output?.map((type, index) => ({
|
||||
name: `output_${index}`,
|
||||
type,
|
||||
is_list: canonical.output_is_list?.[index] || false
|
||||
})) || []
|
||||
)
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
return canonical[prop]
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform V1 property changes back to canonical format
|
||||
*/
|
||||
private static transformV1PropertyToCanonical(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1,
|
||||
value: any
|
||||
): void {
|
||||
switch (prop) {
|
||||
case 'input':
|
||||
canonical.input = value
|
||||
? {
|
||||
required: value.required,
|
||||
optional: value.optional
|
||||
}
|
||||
: undefined
|
||||
break
|
||||
case 'output':
|
||||
canonical.output = value
|
||||
break
|
||||
case 'output_is_list':
|
||||
canonical.output_is_list = value
|
||||
break
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
;(canonical as any)[prop] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform V1.2 property changes back to canonical format
|
||||
*/
|
||||
private static transformV1_2PropertyToCanonical(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV1_2,
|
||||
value: any
|
||||
): void {
|
||||
switch (prop) {
|
||||
case 'inputs':
|
||||
if (Array.isArray(value)) {
|
||||
const required: Record<string, any> = {}
|
||||
const optional: Record<string, any> = {}
|
||||
|
||||
value.forEach((input) => {
|
||||
if (input.required) {
|
||||
required[input.name] = input.spec
|
||||
} else {
|
||||
optional[input.name] = input.spec
|
||||
}
|
||||
})
|
||||
|
||||
canonical.input = {
|
||||
required: Object.keys(required).length > 0 ? required : undefined,
|
||||
optional: Object.keys(optional).length > 0 ? optional : undefined
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'metadata':
|
||||
if (value && typeof value === 'object') {
|
||||
canonical.api_version = value.version
|
||||
canonical.description = value.description || canonical.description
|
||||
}
|
||||
break
|
||||
default:
|
||||
this.transformV1PropertyToCanonical(
|
||||
canonical,
|
||||
prop as keyof ComfyNodeDefV1,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform V3 property changes back to canonical format
|
||||
*/
|
||||
private static transformV3PropertyToCanonical(
|
||||
canonical: ComfyNodeDefLatest,
|
||||
prop: keyof ComfyNodeDefV3,
|
||||
value: any
|
||||
): void {
|
||||
switch (prop) {
|
||||
case 'schema':
|
||||
if (value && typeof value === 'object') {
|
||||
const required: Record<string, any> = {}
|
||||
const optional: Record<string, any> = {}
|
||||
|
||||
Object.entries(value.properties || {}).forEach(([name, spec]) => {
|
||||
if (value.required?.includes(name)) {
|
||||
required[name] = spec
|
||||
} else {
|
||||
optional[name] = spec
|
||||
}
|
||||
})
|
||||
|
||||
canonical.input = {
|
||||
required: Object.keys(required).length > 0 ? required : undefined,
|
||||
optional: Object.keys(optional).length > 0 ? optional : undefined
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'inputs':
|
||||
if (Array.isArray(value)) {
|
||||
const required: Record<string, any> = {}
|
||||
const optional: Record<string, any> = {}
|
||||
|
||||
value.forEach((input) => {
|
||||
if (input.required) {
|
||||
required[input.name] = input.schema
|
||||
} else {
|
||||
optional[input.name] = input.schema
|
||||
}
|
||||
})
|
||||
|
||||
canonical.input = {
|
||||
required: Object.keys(required).length > 0 ? required : undefined,
|
||||
optional: Object.keys(optional).length > 0 ? optional : undefined
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'outputs':
|
||||
if (Array.isArray(value)) {
|
||||
canonical.output = value.map((output) => output.type)
|
||||
canonical.output_is_list = value.map((output) => output.is_list)
|
||||
}
|
||||
break
|
||||
case 'name':
|
||||
case 'display_name':
|
||||
case 'description':
|
||||
case 'category':
|
||||
case 'output_node':
|
||||
case 'python_module':
|
||||
;(canonical as any)[prop] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify about data changes
|
||||
*/
|
||||
private static notifyChange(nodeId: string, prop: string, value: any): void {
|
||||
this.eventBus.dispatchEvent(
|
||||
new CustomEvent('nodedef-changed', {
|
||||
detail: { nodeId, prop, value }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener for data changes
|
||||
*/
|
||||
static addEventListener(type: string, listener: EventListener): void {
|
||||
this.eventBus.addEventListener(type, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event listener
|
||||
*/
|
||||
static removeEventListener(type: string, listener: EventListener): void {
|
||||
this.eventBus.removeEventListener(type, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer type from input specification
|
||||
*/
|
||||
private static inferTypeFromSpec(spec: any): string {
|
||||
if (Array.isArray(spec)) {
|
||||
return 'combo'
|
||||
}
|
||||
|
||||
if (typeof spec === 'object' && spec !== null) {
|
||||
if (spec.type) {
|
||||
return spec.type
|
||||
}
|
||||
if (spec[0] === 'INT') {
|
||||
return 'int'
|
||||
}
|
||||
if (spec[0] === 'FLOAT') {
|
||||
return 'float'
|
||||
}
|
||||
if (spec[0] === 'STRING') {
|
||||
return 'string'
|
||||
}
|
||||
if (spec[0] === 'BOOLEAN') {
|
||||
return 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
250
src/utils/versionTransforms.ts
Normal file
250
src/utils/versionTransforms.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
|
||||
// Type definitions for different API versions
|
||||
export interface ComfyNodeDefV1 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
input?: {
|
||||
required?: Record<string, any>
|
||||
optional?: Record<string, any>
|
||||
}
|
||||
output?: string[]
|
||||
output_is_list?: boolean[]
|
||||
python_module?: string
|
||||
}
|
||||
|
||||
export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 {
|
||||
inputs?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
options?: any
|
||||
spec?: any
|
||||
}>
|
||||
metadata?: {
|
||||
version?: string
|
||||
author?: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComfyNodeDefV3 {
|
||||
name: string
|
||||
display_name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
output_node?: boolean
|
||||
schema: {
|
||||
type: 'object'
|
||||
properties: Record<string, any>
|
||||
required?: string[]
|
||||
}
|
||||
inputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
schema: any
|
||||
}>
|
||||
outputs: Array<{
|
||||
name: string
|
||||
type: string
|
||||
is_list: boolean
|
||||
}>
|
||||
python_module?: string
|
||||
}
|
||||
|
||||
// Use current ComfyNodeDef as the canonical format
|
||||
export type ComfyNodeDefLatest = ComfyNodeDef
|
||||
|
||||
/**
|
||||
* Transforms node definitions between different API versions
|
||||
*/
|
||||
export class VersionTransforms {
|
||||
/**
|
||||
* Transform API responses to all supported versions
|
||||
*/
|
||||
static transformToAllVersions(currentData: any, v3Data: any = null) {
|
||||
// Use current data as canonical since it's our primary source
|
||||
const canonical = currentData || {}
|
||||
|
||||
return {
|
||||
canonical,
|
||||
v1: this.canonicalToV1(canonical),
|
||||
v1_2: this.canonicalToV1_2(canonical),
|
||||
v3: v3Data || this.canonicalToV3(canonical)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical format to v1 format
|
||||
*/
|
||||
static canonicalToV1(
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
): Record<string, ComfyNodeDefV1> {
|
||||
const v1Nodes: Record<string, ComfyNodeDefV1> = {}
|
||||
|
||||
for (const [nodeName, nodeData] of Object.entries(canonical)) {
|
||||
v1Nodes[nodeName] = {
|
||||
name: nodeData.name,
|
||||
display_name: nodeData.display_name,
|
||||
description: nodeData.description,
|
||||
category: nodeData.category,
|
||||
output_node: nodeData.output_node,
|
||||
python_module: nodeData.python_module,
|
||||
input: nodeData.input
|
||||
? {
|
||||
required: nodeData.input.required,
|
||||
optional: nodeData.input.optional
|
||||
}
|
||||
: undefined,
|
||||
output: nodeData.output,
|
||||
output_is_list: nodeData.output_is_list
|
||||
}
|
||||
}
|
||||
|
||||
return v1Nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical format to v1.2 format
|
||||
*/
|
||||
static canonicalToV1_2(
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
): Record<string, ComfyNodeDefV1_2> {
|
||||
const v1_2Nodes: Record<string, ComfyNodeDefV1_2> = {}
|
||||
|
||||
for (const [nodeName, nodeData] of Object.entries(canonical)) {
|
||||
const v1Node = this.canonicalToV1({ [nodeName]: nodeData })[nodeName]
|
||||
|
||||
v1_2Nodes[nodeName] = {
|
||||
...v1Node,
|
||||
inputs: nodeData.input
|
||||
? [
|
||||
...Object.entries(nodeData.input.required || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: true,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
),
|
||||
...Object.entries(nodeData.input.optional || {}).map(
|
||||
([name, spec]) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(spec),
|
||||
required: false,
|
||||
spec,
|
||||
options: Array.isArray(spec) ? spec : undefined
|
||||
})
|
||||
)
|
||||
]
|
||||
: undefined,
|
||||
metadata: {
|
||||
version: nodeData.api_version,
|
||||
author: 'Unknown',
|
||||
description: nodeData.description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v1_2Nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform canonical format to v3 format
|
||||
*/
|
||||
static canonicalToV3(
|
||||
canonical: Record<string, ComfyNodeDefLatest>
|
||||
): Record<string, ComfyNodeDefV3> {
|
||||
const v3Nodes: Record<string, ComfyNodeDefV3> = {}
|
||||
|
||||
for (const [nodeName, nodeData] of Object.entries(canonical)) {
|
||||
const requiredInputs = Object.keys(nodeData.input?.required || {})
|
||||
const optionalInputs = Object.keys(nodeData.input?.optional || {})
|
||||
|
||||
v3Nodes[nodeName] = {
|
||||
name: nodeData.name,
|
||||
display_name: nodeData.display_name,
|
||||
description: nodeData.description,
|
||||
category: nodeData.category,
|
||||
output_node: nodeData.output_node,
|
||||
python_module: nodeData.python_module,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(nodeData.input?.required || {}),
|
||||
...(nodeData.input?.optional || {})
|
||||
},
|
||||
required: requiredInputs
|
||||
},
|
||||
inputs: [
|
||||
...requiredInputs.map((name) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(nodeData.input!.required![name]),
|
||||
required: true,
|
||||
schema: nodeData.input!.required![name]
|
||||
})),
|
||||
...optionalInputs.map((name) => ({
|
||||
name,
|
||||
type: this.inferTypeFromSpec(nodeData.input!.optional![name]),
|
||||
required: false,
|
||||
schema: nodeData.input!.optional![name]
|
||||
}))
|
||||
],
|
||||
outputs:
|
||||
nodeData.output?.map((type, index) => ({
|
||||
name: `output_${index}`,
|
||||
type,
|
||||
is_list: nodeData.output_is_list?.[index] || false
|
||||
})) || []
|
||||
}
|
||||
}
|
||||
|
||||
return v3Nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer type from input specification
|
||||
*/
|
||||
private static inferTypeFromSpec(spec: any): string {
|
||||
if (Array.isArray(spec)) {
|
||||
return 'combo'
|
||||
}
|
||||
|
||||
if (typeof spec === 'object' && spec !== null) {
|
||||
if (spec.type) {
|
||||
return spec.type
|
||||
}
|
||||
if (spec[0] === 'INT') {
|
||||
return 'int'
|
||||
}
|
||||
if (spec[0] === 'FLOAT') {
|
||||
return 'float'
|
||||
}
|
||||
if (spec[0] === 'STRING') {
|
||||
return 'string'
|
||||
}
|
||||
if (spec[0] === 'BOOLEAN') {
|
||||
return 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge current and v3 data to create canonical format
|
||||
*/
|
||||
private static createCanonicalFormat(
|
||||
currentData: any,
|
||||
v3Data: any
|
||||
): Record<string, ComfyNodeDefLatest> {
|
||||
// For now, use current data as canonical
|
||||
// In the future, we might merge v3 data for enhanced information
|
||||
return currentData || {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user