mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
1337 lines
41 KiB
TypeScript
1337 lines
41 KiB
TypeScript
import { uniqBy } from 'es-toolkit/compat'
|
|
import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue'
|
|
|
|
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
|
|
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
|
import config from '@/config'
|
|
import { useComfyManagerService } from '@/services/comfyManagerService'
|
|
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
|
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
|
import { useConflictDetectionStore } from '@/stores/conflictDetectionStore'
|
|
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
|
import type { SystemStats } from '@/types'
|
|
import type { components } from '@/types/comfyRegistryTypes'
|
|
import type {
|
|
ConflictDetail,
|
|
ConflictDetectionResponse,
|
|
ConflictDetectionResult,
|
|
ConflictDetectionSummary,
|
|
ConflictType,
|
|
Node,
|
|
NodePackRequirements,
|
|
SystemEnvironment
|
|
} from '@/types/conflictDetectionTypes'
|
|
import {
|
|
cleanVersion,
|
|
satisfiesVersion,
|
|
utilCheckVersionCompatibility
|
|
} from '@/utils/versionUtil'
|
|
|
|
/**
|
|
* Composable for conflict detection system.
|
|
* Error-resilient and asynchronous to avoid affecting other components.
|
|
*/
|
|
export function useConflictDetection() {
|
|
const managerStore = useComfyManagerStore()
|
|
|
|
const {
|
|
startFetchInstalled,
|
|
installedPacks,
|
|
installedPacksWithVersions,
|
|
isReady: installedPacksReady
|
|
} = useInstalledPacks()
|
|
|
|
const isDetecting = ref(false)
|
|
const lastDetectionTime = ref<string | null>(null)
|
|
const detectionError = ref<string | null>(null)
|
|
|
|
const systemEnvironment = ref<SystemEnvironment | null>(null)
|
|
|
|
const detectionResults = ref<ConflictDetectionResult[]>([])
|
|
// Store merged conflicts separately for testing
|
|
const storedMergedConflicts = ref<ConflictDetectionResult[]>([])
|
|
const detectionSummary = ref<ConflictDetectionSummary | null>(null)
|
|
|
|
// Registry API request cancellation
|
|
const abortController = ref<AbortController | null>(null)
|
|
|
|
const acknowledgment = useConflictAcknowledgment()
|
|
|
|
const conflictStore = useConflictDetectionStore()
|
|
|
|
const hasConflicts = computed(() => conflictStore.hasConflicts)
|
|
const conflictedPackages = computed(() => {
|
|
return conflictStore.conflictedPackages
|
|
})
|
|
|
|
const bannedPackages = computed(() => conflictStore.bannedPackages)
|
|
const securityPendingPackages = computed(
|
|
() => conflictStore.securityPendingPackages
|
|
)
|
|
|
|
/**
|
|
* Collects current system environment information.
|
|
* Continues with default values even if errors occur.
|
|
* @returns Promise that resolves to system environment information
|
|
*/
|
|
async function detectSystemEnvironment(): Promise<SystemEnvironment> {
|
|
try {
|
|
// Get system stats from store (primary source of system information)
|
|
const systemStatsStore = useSystemStatsStore()
|
|
if (!systemStatsStore.systemStats) {
|
|
await systemStatsStore.fetchSystemStats()
|
|
}
|
|
|
|
// Fetch version information from backend (with error resilience)
|
|
const [frontendVersion] = await Promise.allSettled([
|
|
fetchFrontendVersion()
|
|
])
|
|
|
|
// Extract system information from system stats
|
|
const systemStats = systemStatsStore.systemStats
|
|
const comfyuiVersion = systemStats?.system?.comfyui_version || 'unknown'
|
|
|
|
// Use system stats for OS detection (more accurate than browser detection)
|
|
const systemOS = systemStats?.system?.os || 'unknown'
|
|
|
|
// Extract architecture from system stats device information
|
|
const architecture = extractArchitectureFromSystemStats(systemStats)
|
|
|
|
// Detect GPU/accelerator information from system stats
|
|
const acceleratorInfo = extractAcceleratorInfo(systemStats)
|
|
|
|
// Enhanced OS detection using multiple sources
|
|
const detectedOS = detectOSFromSystemStats(systemOS, systemStats)
|
|
|
|
const environment: SystemEnvironment = {
|
|
// Version information (use 'unknown' on failure)
|
|
comfyui_version: comfyuiVersion,
|
|
frontend_version:
|
|
frontendVersion.status === 'fulfilled'
|
|
? frontendVersion.value
|
|
: 'unknown',
|
|
|
|
// Platform information (from system stats)
|
|
os: detectedOS,
|
|
platform_details: systemOS,
|
|
architecture: architecture,
|
|
|
|
// GPU/accelerator information
|
|
available_accelerators: acceleratorInfo.available,
|
|
primary_accelerator: acceleratorInfo.primary,
|
|
gpu_memory_mb: acceleratorInfo.memory_mb,
|
|
|
|
// Runtime information
|
|
node_env: import.meta.env.MODE as 'development' | 'production',
|
|
user_agent: navigator.userAgent
|
|
}
|
|
|
|
systemEnvironment.value = environment
|
|
console.log(
|
|
'[ConflictDetection] System environment detection completed:',
|
|
environment
|
|
)
|
|
return environment
|
|
} catch (error) {
|
|
console.warn(
|
|
'[ConflictDetection] Error during system environment detection:',
|
|
error
|
|
)
|
|
|
|
// Try to get frontend version even in fallback mode
|
|
let frontendVersion = 'unknown'
|
|
try {
|
|
frontendVersion = await fetchFrontendVersion()
|
|
} catch {
|
|
frontendVersion = 'unknown'
|
|
}
|
|
|
|
// Provide basic environment information even on error
|
|
const fallbackEnvironment: SystemEnvironment = {
|
|
comfyui_version: 'unknown',
|
|
frontend_version: frontendVersion,
|
|
os: detectOSFromSystemStats(navigator.platform),
|
|
platform_details: navigator.platform,
|
|
architecture: getArchitecture(),
|
|
available_accelerators: ['CPU'],
|
|
primary_accelerator: 'CPU',
|
|
node_env: import.meta.env.MODE as 'development' | 'production',
|
|
user_agent: navigator.userAgent
|
|
}
|
|
|
|
systemEnvironment.value = fallbackEnvironment
|
|
return fallbackEnvironment
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches requirement information for installed packages using Registry Store.
|
|
*
|
|
* This function combines local installation data with Registry API compatibility metadata
|
|
* using the established store layer pattern with caching and batch requests.
|
|
*
|
|
* Process
|
|
* 1. Get locally installed packages
|
|
* 2. Batch fetch Registry data using store layer
|
|
* 3. Combine local + Registry data
|
|
* 4. Extract compatibility requirements
|
|
*
|
|
* @returns Promise that resolves to array of node pack requirements
|
|
*/
|
|
async function fetchPackageRequirements(): Promise<NodePackRequirements[]> {
|
|
try {
|
|
// Step 1: Use installed packs composable instead of direct API calls
|
|
await startFetchInstalled() // Ensure data is loaded
|
|
|
|
if (
|
|
!installedPacksReady.value ||
|
|
!installedPacks.value ||
|
|
installedPacks.value.length === 0
|
|
) {
|
|
console.warn(
|
|
'[ConflictDetection] No installed packages available from useInstalledPacks'
|
|
)
|
|
return []
|
|
}
|
|
|
|
// Step 2: Get Registry service for bulk API calls
|
|
const registryService = useComfyRegistryService()
|
|
|
|
// Step 3: Setup abort controller for request cancellation
|
|
abortController.value = new AbortController()
|
|
|
|
// Step 4: Use bulk API to fetch all version data in a single request
|
|
const versionDataMap = new Map<
|
|
string,
|
|
components['schemas']['NodeVersion']
|
|
>()
|
|
|
|
// Prepare bulk request with actual installed versions from Manager API
|
|
const nodeVersions = installedPacksWithVersions.value.map((pack) => ({
|
|
node_id: pack.id,
|
|
version: pack.version
|
|
}))
|
|
|
|
if (nodeVersions.length > 0) {
|
|
try {
|
|
const bulkResponse = await registryService.getBulkNodeVersions(
|
|
nodeVersions,
|
|
abortController.value?.signal
|
|
)
|
|
|
|
if (bulkResponse && bulkResponse.node_versions) {
|
|
// Process bulk response
|
|
bulkResponse.node_versions.forEach((result) => {
|
|
if (result.status === 'success' && result.node_version) {
|
|
versionDataMap.set(
|
|
result.identifier.node_id,
|
|
result.node_version
|
|
)
|
|
} else if (result.status === 'error') {
|
|
console.warn(
|
|
`[ConflictDetection] Failed to fetch version data for ${result.identifier.node_id}@${result.identifier.version}:`,
|
|
result.error_message
|
|
)
|
|
}
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
'[ConflictDetection] Failed to fetch bulk version data:',
|
|
error
|
|
)
|
|
}
|
|
}
|
|
|
|
// Step 5: Combine local installation data with Registry version data
|
|
const requirements: NodePackRequirements[] = []
|
|
|
|
// IMPORTANT: Use installedPacksWithVersions to check ALL installed packages
|
|
// not just the ones that exist in Registry (installedPacks)
|
|
for (const installedPack of installedPacksWithVersions.value) {
|
|
const packageId = installedPack.id
|
|
const versionData = versionDataMap.get(packageId)
|
|
const installedVersion = installedPack.version || 'unknown'
|
|
|
|
// Check if package is enabled using store method
|
|
const isEnabled = managerStore.isPackEnabled(packageId)
|
|
|
|
// Find the pack info from Registry if available
|
|
const packInfo = installedPacks.value.find((p) => p.id === packageId)
|
|
|
|
if (versionData) {
|
|
// Combine local installation data with version-specific Registry data
|
|
const requirement: NodePackRequirements = {
|
|
// Basic package info
|
|
id: packageId,
|
|
name: packInfo?.name || packageId,
|
|
installed_version: installedVersion,
|
|
is_enabled: isEnabled,
|
|
|
|
// Version-specific compatibility data
|
|
supported_comfyui_version: versionData.supported_comfyui_version,
|
|
supported_comfyui_frontend_version:
|
|
versionData.supported_comfyui_frontend_version,
|
|
supported_os: normalizeOSValues(versionData.supported_os),
|
|
supported_accelerators: versionData.supported_accelerators,
|
|
|
|
// Status information
|
|
version_status: versionData.status,
|
|
is_banned: versionData.status === 'NodeVersionStatusBanned',
|
|
is_pending: versionData.status === 'NodeVersionStatusPending'
|
|
}
|
|
|
|
requirements.push(requirement)
|
|
} else {
|
|
console.warn(
|
|
`[ConflictDetection] No Registry data found for ${packageId}, using fallback`
|
|
)
|
|
|
|
// Create fallback requirement without Registry data
|
|
const fallbackRequirement: NodePackRequirements = {
|
|
id: packageId,
|
|
name: packInfo?.name || packageId,
|
|
installed_version: installedVersion,
|
|
is_enabled: isEnabled,
|
|
is_banned: false,
|
|
is_pending: false
|
|
}
|
|
|
|
requirements.push(fallbackRequirement)
|
|
}
|
|
}
|
|
|
|
return requirements
|
|
} catch (error) {
|
|
console.warn(
|
|
'[ConflictDetection] Failed to fetch package requirements:',
|
|
error
|
|
)
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detects conflicts for an individual package using Registry API data.
|
|
*
|
|
* @param packageReq Package requirements from Registry
|
|
* @param sysEnv Current system environment
|
|
* @returns Conflict detection result for the package
|
|
*/
|
|
function detectPackageConflicts(
|
|
packageReq: NodePackRequirements,
|
|
sysEnv: SystemEnvironment
|
|
): ConflictDetectionResult {
|
|
const conflicts: ConflictDetail[] = []
|
|
|
|
// Helper function to check if a value indicates "compatible with all"
|
|
const isCompatibleWithAll = (value: any): boolean => {
|
|
if (value === null || value === undefined) return true
|
|
if (typeof value === 'string' && value.trim() === '') return true
|
|
if (Array.isArray(value) && value.length === 0) return true
|
|
return false
|
|
}
|
|
|
|
// 1. ComfyUI version conflict check
|
|
if (!isCompatibleWithAll(packageReq.supported_comfyui_version)) {
|
|
const versionConflict = checkVersionConflict(
|
|
'comfyui_version',
|
|
sysEnv.comfyui_version,
|
|
packageReq.supported_comfyui_version!
|
|
)
|
|
if (versionConflict) conflicts.push(versionConflict)
|
|
}
|
|
|
|
// 2. Frontend version conflict check
|
|
if (!isCompatibleWithAll(packageReq.supported_comfyui_frontend_version)) {
|
|
const versionConflict = checkVersionConflict(
|
|
'frontend_version',
|
|
sysEnv.frontend_version,
|
|
packageReq.supported_comfyui_frontend_version!
|
|
)
|
|
if (versionConflict) conflicts.push(versionConflict)
|
|
}
|
|
|
|
// 3. OS compatibility check
|
|
if (!isCompatibleWithAll(packageReq.supported_os)) {
|
|
const osConflict = checkOSConflict(packageReq.supported_os!, sysEnv.os)
|
|
if (osConflict) conflicts.push(osConflict)
|
|
}
|
|
|
|
// 4. Accelerator compatibility check
|
|
if (!isCompatibleWithAll(packageReq.supported_accelerators)) {
|
|
const acceleratorConflict = checkAcceleratorConflict(
|
|
packageReq.supported_accelerators!,
|
|
sysEnv.available_accelerators
|
|
)
|
|
if (acceleratorConflict) conflicts.push(acceleratorConflict)
|
|
}
|
|
|
|
// 5. Banned package check using shared logic
|
|
const bannedConflict = checkBannedStatus(packageReq.is_banned)
|
|
if (bannedConflict) {
|
|
conflicts.push(bannedConflict)
|
|
}
|
|
|
|
// 6. Registry data availability check using shared logic
|
|
const pendingConflict = checkPendingStatus(packageReq.is_pending)
|
|
if (pendingConflict) {
|
|
conflicts.push(pendingConflict)
|
|
}
|
|
|
|
// Generate result
|
|
const hasConflict = conflicts.length > 0
|
|
|
|
return {
|
|
package_id: packageReq.id ?? '',
|
|
package_name: packageReq.name ?? '',
|
|
has_conflict: hasConflict,
|
|
conflicts,
|
|
is_compatible: !hasConflict
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches Python import failure information from ComfyUI Manager.
|
|
* Gets installed packages and checks each one for import failures using bulk API.
|
|
* @returns Promise that resolves to import failure data
|
|
*/
|
|
async function fetchImportFailInfo(): Promise<Record<string, any>> {
|
|
try {
|
|
const comfyManagerService = useComfyManagerService()
|
|
|
|
// Use installedPacksWithVersions to match what versions bulk API uses
|
|
// This ensures both APIs check the same set of packages
|
|
if (
|
|
!installedPacksWithVersions.value ||
|
|
installedPacksWithVersions.value.length === 0
|
|
) {
|
|
console.warn(
|
|
'[ConflictDetection] No installed packages available for import failure check'
|
|
)
|
|
return {}
|
|
}
|
|
|
|
const packageIds = installedPacksWithVersions.value.map((pack) => pack.id)
|
|
|
|
// Use bulk API to get import failure info for all packages at once
|
|
const bulkResult = await comfyManagerService.getImportFailInfoBulk(
|
|
{ cnr_ids: packageIds },
|
|
abortController.value?.signal
|
|
)
|
|
|
|
if (bulkResult) {
|
|
// Filter out null values (packages without import failures)
|
|
const importFailures: Record<string, any> = {}
|
|
|
|
Object.entries(bulkResult).forEach(([packageId, failInfo]) => {
|
|
if (failInfo !== null) {
|
|
importFailures[packageId] = failInfo
|
|
console.log(
|
|
`[ConflictDetection] Import failure found for ${packageId}:`,
|
|
failInfo
|
|
)
|
|
}
|
|
})
|
|
|
|
return importFailures
|
|
}
|
|
|
|
return {}
|
|
} catch (error) {
|
|
console.warn(
|
|
'[ConflictDetection] Failed to fetch import failure information:',
|
|
error
|
|
)
|
|
return {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detects runtime conflicts from Python import failures.
|
|
* @param importFailInfo Import failure data from Manager API
|
|
* @returns Array of conflict detection results for failed imports
|
|
*/
|
|
function detectImportFailConflicts(
|
|
importFailInfo: Record<string, { msg: string; name: string; path: string }>
|
|
): ConflictDetectionResult[] {
|
|
const results: ConflictDetectionResult[] = []
|
|
if (!importFailInfo || typeof importFailInfo !== 'object') {
|
|
return results
|
|
}
|
|
|
|
// Process import failures
|
|
for (const [packageId, failureInfo] of Object.entries(importFailInfo)) {
|
|
if (failureInfo && typeof failureInfo === 'object') {
|
|
// Extract error information from Manager API response
|
|
const errorMsg = failureInfo.msg || 'Unknown import error'
|
|
const modulePath = failureInfo.path || ''
|
|
|
|
results.push({
|
|
package_id: packageId,
|
|
package_name: packageId,
|
|
has_conflict: true,
|
|
conflicts: [
|
|
{
|
|
type: 'import_failed',
|
|
current_value: 'installed',
|
|
required_value: failureInfo.msg
|
|
}
|
|
],
|
|
is_compatible: false
|
|
})
|
|
|
|
console.warn(
|
|
`[ConflictDetection] Python import failure detected for ${packageId}:`,
|
|
{
|
|
path: modulePath,
|
|
error: errorMsg
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* Performs complete conflict detection.
|
|
* @returns Promise that resolves to conflict detection response
|
|
*/
|
|
async function performConflictDetection(): Promise<ConflictDetectionResponse> {
|
|
if (isDetecting.value) {
|
|
console.log('[ConflictDetection] Already detecting, skipping')
|
|
return {
|
|
success: false,
|
|
error_message: 'Already detecting conflicts',
|
|
summary: detectionSummary.value!,
|
|
results: detectionResults.value
|
|
}
|
|
}
|
|
|
|
isDetecting.value = true
|
|
detectionError.value = null
|
|
const startTime = Date.now()
|
|
|
|
try {
|
|
// 1. Collect system environment information
|
|
const sysEnv = await detectSystemEnvironment()
|
|
|
|
// 2. Collect package requirement information
|
|
const packageRequirements = await fetchPackageRequirements()
|
|
|
|
// 3. Detect conflicts for each package (parallel processing)
|
|
const conflictDetectionTasks = packageRequirements.map(
|
|
async (packageReq) => {
|
|
try {
|
|
return detectPackageConflicts(packageReq, sysEnv)
|
|
} catch (error) {
|
|
console.warn(
|
|
`[ConflictDetection] Failed to detect conflicts for package ${packageReq.name}:`,
|
|
error
|
|
)
|
|
// Return null for failed packages, will be filtered out
|
|
return null
|
|
}
|
|
}
|
|
)
|
|
|
|
const conflictResults = await Promise.allSettled(conflictDetectionTasks)
|
|
const packageResults: ConflictDetectionResult[] = conflictResults
|
|
.map((result) => (result.status === 'fulfilled' ? result.value : null))
|
|
.filter((result): result is ConflictDetectionResult => result !== null)
|
|
|
|
// 4. Detect Python import failures
|
|
const importFailInfo = await fetchImportFailInfo()
|
|
const importFailResults = detectImportFailConflicts(importFailInfo)
|
|
|
|
// 5. Combine all results
|
|
const allResults = [...packageResults, ...importFailResults]
|
|
|
|
// 6. Generate summary information
|
|
const summary = generateSummary(allResults, Date.now() - startTime)
|
|
|
|
// 7. Update state
|
|
detectionResults.value = allResults
|
|
detectionSummary.value = summary
|
|
lastDetectionTime.value = new Date().toISOString()
|
|
|
|
console.log('[ConflictDetection] Conflict detection completed:', summary)
|
|
|
|
// Store conflict results for later UI display
|
|
// Dialog will be shown based on specific events, not on app mount
|
|
if (allResults.some((result) => result.has_conflict)) {
|
|
const conflictedResults = allResults.filter(
|
|
(result) => result.has_conflict
|
|
)
|
|
|
|
// Merge conflicts for packages with the same name
|
|
const mergedConflicts = mergeConflictsByPackageName(conflictedResults)
|
|
|
|
console.log(
|
|
'[ConflictDetection] Conflicts detected (stored for UI):',
|
|
mergedConflicts
|
|
)
|
|
|
|
// Store merged conflicts in Pinia store for UI usage
|
|
conflictStore.setConflictedPackages(mergedConflicts)
|
|
|
|
// Also update local state for backward compatibility
|
|
detectionResults.value.splice(
|
|
0,
|
|
detectionResults.value.length,
|
|
...mergedConflicts
|
|
)
|
|
storedMergedConflicts.value = [...mergedConflicts]
|
|
|
|
// Use merged conflicts in response as well
|
|
const response: ConflictDetectionResponse = {
|
|
success: true,
|
|
summary,
|
|
results: mergedConflicts,
|
|
detected_system_environment: sysEnv
|
|
}
|
|
return response
|
|
} else {
|
|
// No conflicts detected, clear the results
|
|
conflictStore.clearConflicts()
|
|
detectionResults.value = []
|
|
}
|
|
|
|
const response: ConflictDetectionResponse = {
|
|
success: true,
|
|
summary,
|
|
results: allResults,
|
|
detected_system_environment: sysEnv
|
|
}
|
|
|
|
return response
|
|
} catch (error) {
|
|
console.error(
|
|
'[ConflictDetection] Error during conflict detection:',
|
|
error
|
|
)
|
|
detectionError.value =
|
|
error instanceof Error ? error.message : String(error)
|
|
|
|
return {
|
|
success: false,
|
|
error_message: detectionError.value,
|
|
summary: detectionSummary.value || getEmptySummary(),
|
|
results: []
|
|
}
|
|
} finally {
|
|
isDetecting.value = false
|
|
// Clear abort controller to prevent memory leaks
|
|
if (abortController.value) {
|
|
abortController.value = null
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error-resilient initialization (called on app mount).
|
|
* Async function that doesn't block UI setup.
|
|
* Ensures proper order: installed -> system_stats -> versions bulk -> import_fail_info_bulk
|
|
*/
|
|
async function initializeConflictDetection(): Promise<void> {
|
|
try {
|
|
// Simply perform conflict detection
|
|
// The useInstalledPacks will handle fetching installed list if needed
|
|
await performConflictDetection()
|
|
} catch (error) {
|
|
console.warn(
|
|
'[ConflictDetection] Error during initialization (ignored):',
|
|
error
|
|
)
|
|
// Errors do not affect other parts of the app
|
|
}
|
|
}
|
|
|
|
// Cleanup function for request cancellation
|
|
function cancelRequests(): void {
|
|
if (abortController.value) {
|
|
abortController.value.abort()
|
|
abortController.value = null
|
|
}
|
|
}
|
|
|
|
// Auto-cleanup on component unmount
|
|
// Only register lifecycle hooks if we're in a Vue component context
|
|
const instance = getCurrentInstance()
|
|
if (instance) {
|
|
onUnmounted(() => {
|
|
cancelRequests()
|
|
})
|
|
}
|
|
|
|
// Helper functions (implementations at the bottom of the file)
|
|
|
|
/**
|
|
* Check if conflicts should trigger modal display after "What's New" dismissal
|
|
*/
|
|
async function shouldShowConflictModalAfterUpdate(): Promise<boolean> {
|
|
console.log(
|
|
'[ConflictDetection] Checking if conflict modal should show after update...'
|
|
)
|
|
|
|
// Ensure conflict detection has run
|
|
if (detectionResults.value.length === 0) {
|
|
console.log(
|
|
'[ConflictDetection] No detection results, running conflict detection...'
|
|
)
|
|
await performConflictDetection()
|
|
}
|
|
|
|
// Check if this is a version update scenario
|
|
// In a real scenario, this would check actual version change
|
|
// For now, we'll assume it's an update if we have conflicts and modal hasn't been dismissed
|
|
const hasActualConflicts = hasConflicts.value
|
|
const canShowModal = acknowledgment.shouldShowConflictModal.value
|
|
|
|
console.log('[ConflictDetection] Modal check:', {
|
|
hasConflicts: hasActualConflicts,
|
|
canShowModal: canShowModal,
|
|
conflictedPackagesCount: conflictedPackages.value.length
|
|
})
|
|
|
|
return hasActualConflicts && canShowModal
|
|
}
|
|
|
|
/**
|
|
* Check compatibility for a node.
|
|
* Used by components like PackVersionSelectorPopover.
|
|
*/
|
|
function checkNodeCompatibility(
|
|
node: Node | components['schemas']['NodeVersion']
|
|
) {
|
|
const systemStatsStore = useSystemStatsStore()
|
|
const systemStats = systemStatsStore.systemStats
|
|
if (!systemStats) return { hasConflict: false, conflicts: [] }
|
|
|
|
const conflicts: ConflictDetail[] = []
|
|
|
|
// Check OS compatibility using centralized function
|
|
if (node.supported_os && node.supported_os.length > 0) {
|
|
const currentOS = systemStats.system?.os || 'unknown'
|
|
const osConflict = checkOSConflict(node.supported_os, currentOS)
|
|
if (osConflict) {
|
|
conflicts.push(osConflict)
|
|
}
|
|
}
|
|
|
|
// Check accelerator compatibility using centralized function
|
|
if (node.supported_accelerators && node.supported_accelerators.length > 0) {
|
|
// Extract available accelerators from system stats
|
|
const acceleratorInfo = extractAcceleratorInfo(systemStats)
|
|
const availableAccelerators: Node['supported_accelerators'] = []
|
|
|
|
acceleratorInfo.available?.forEach((accel) => {
|
|
if (accel === 'CUDA') availableAccelerators.push('CUDA')
|
|
if (accel === 'Metal') availableAccelerators.push('Metal')
|
|
if (accel === 'CPU') availableAccelerators.push('CPU')
|
|
})
|
|
|
|
const acceleratorConflict = checkAcceleratorConflict(
|
|
node.supported_accelerators,
|
|
availableAccelerators
|
|
)
|
|
if (acceleratorConflict) {
|
|
conflicts.push(acceleratorConflict)
|
|
}
|
|
}
|
|
|
|
// Check ComfyUI version compatibility
|
|
if (node.supported_comfyui_version) {
|
|
const currentComfyUIVersion = systemStats.system?.comfyui_version
|
|
if (currentComfyUIVersion && currentComfyUIVersion !== 'unknown') {
|
|
const versionConflict = utilCheckVersionCompatibility(
|
|
'comfyui_version',
|
|
currentComfyUIVersion,
|
|
node.supported_comfyui_version
|
|
)
|
|
if (versionConflict) {
|
|
conflicts.push(versionConflict)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check ComfyUI Frontend version compatibility
|
|
if (node.supported_comfyui_frontend_version) {
|
|
const currentFrontendVersion = config.app_version
|
|
if (currentFrontendVersion && currentFrontendVersion !== 'unknown') {
|
|
const versionConflict = utilCheckVersionCompatibility(
|
|
'frontend_version',
|
|
currentFrontendVersion,
|
|
node.supported_comfyui_frontend_version
|
|
)
|
|
if (versionConflict) {
|
|
conflicts.push(versionConflict)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check banned package status using shared logic
|
|
const bannedConflict = checkBannedStatus(
|
|
node.status === 'NodeStatusBanned' ||
|
|
node.status === 'NodeVersionStatusBanned'
|
|
)
|
|
if (bannedConflict) {
|
|
conflicts.push(bannedConflict)
|
|
}
|
|
|
|
// Check pending status using shared logic
|
|
const pendingConflict = checkPendingStatus(
|
|
node.status === 'NodeVersionStatusPending'
|
|
)
|
|
if (pendingConflict) {
|
|
conflicts.push(pendingConflict)
|
|
}
|
|
|
|
return {
|
|
hasConflict: conflicts.length > 0,
|
|
conflicts
|
|
}
|
|
}
|
|
|
|
return {
|
|
// State
|
|
isDetecting: readonly(isDetecting),
|
|
lastDetectionTime: readonly(lastDetectionTime),
|
|
detectionError: readonly(detectionError),
|
|
systemEnvironment: readonly(systemEnvironment),
|
|
detectionResults: readonly(detectionResults),
|
|
detectionSummary: readonly(detectionSummary),
|
|
|
|
// Computed
|
|
hasConflicts,
|
|
conflictedPackages,
|
|
bannedPackages,
|
|
securityPendingPackages,
|
|
|
|
// Methods
|
|
performConflictDetection,
|
|
detectSystemEnvironment,
|
|
initializeConflictDetection,
|
|
cancelRequests,
|
|
shouldShowConflictModalAfterUpdate,
|
|
|
|
// Helper functions for other components
|
|
checkNodeCompatibility
|
|
}
|
|
}
|
|
|
|
// Helper Functions Implementation
|
|
|
|
/**
|
|
* Merges conflict results for packages with the same name.
|
|
* Combines all conflicts from different detection sources (registry, python, extension)
|
|
* into a single result per package name.
|
|
* @param conflicts Array of conflict detection results
|
|
* @returns Array of merged conflict detection results
|
|
*/
|
|
function mergeConflictsByPackageName(
|
|
conflicts: ConflictDetectionResult[]
|
|
): ConflictDetectionResult[] {
|
|
const mergedMap = new Map<string, ConflictDetectionResult>()
|
|
|
|
conflicts.forEach((conflict) => {
|
|
// Normalize package name by removing version suffix (@1_0_3) for consistent merging
|
|
const normalizedPackageName = conflict.package_name.includes('@')
|
|
? conflict.package_name.substring(0, conflict.package_name.indexOf('@'))
|
|
: conflict.package_name
|
|
|
|
if (mergedMap.has(normalizedPackageName)) {
|
|
// Package already exists, merge conflicts
|
|
const existing = mergedMap.get(normalizedPackageName)!
|
|
|
|
// Combine all conflicts, avoiding duplicates using es-toolkit uniqBy for O(n) performance
|
|
const allConflicts = [...existing.conflicts, ...conflict.conflicts]
|
|
const uniqueConflicts = uniqBy(
|
|
allConflicts,
|
|
(conflict) =>
|
|
`${conflict.type}|${conflict.current_value}|${conflict.required_value}`
|
|
)
|
|
|
|
// Update the existing entry with normalized package name
|
|
mergedMap.set(normalizedPackageName, {
|
|
...existing,
|
|
package_name: normalizedPackageName,
|
|
conflicts: uniqueConflicts,
|
|
has_conflict: uniqueConflicts.length > 0,
|
|
is_compatible: uniqueConflicts.length === 0
|
|
})
|
|
} else {
|
|
// New package, add with normalized package name
|
|
mergedMap.set(normalizedPackageName, {
|
|
...conflict,
|
|
package_name: normalizedPackageName
|
|
})
|
|
}
|
|
})
|
|
|
|
return Array.from(mergedMap.values())
|
|
}
|
|
|
|
/**
|
|
* Fetches frontend version from config.
|
|
* @returns Promise that resolves to frontend version string
|
|
*/
|
|
async function fetchFrontendVersion(): Promise<string> {
|
|
try {
|
|
// Get frontend version from vite build-time constant or fallback to config
|
|
return config.app_version || import.meta.env.VITE_APP_VERSION || 'unknown'
|
|
} catch {
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detects system architecture from user agent.
|
|
* Note: Browser architecture detection has limitations and may not be 100% accurate.
|
|
* @returns Architecture string
|
|
*/
|
|
function getArchitecture(): string {
|
|
const ua = navigator.userAgent.toLowerCase()
|
|
if (ua.includes('arm64') || ua.includes('aarch64')) return 'arm64'
|
|
if (ua.includes('arm')) return 'arm'
|
|
if (ua.includes('x86_64') || ua.includes('x64')) return 'x64'
|
|
if (ua.includes('x86')) return 'x86'
|
|
return 'unknown'
|
|
}
|
|
|
|
/**
|
|
* Normalizes OS values from Registry API to match our SupportedOS type.
|
|
* Registry Admin guide specifies: Windows, macOS, Linux
|
|
* @param osValues OS values from Registry API
|
|
* @returns Normalized OS values
|
|
*/
|
|
function normalizeOSValues(
|
|
osValues: string[] | undefined
|
|
): Node['supported_os'] {
|
|
if (!osValues || osValues.length === 0) {
|
|
return []
|
|
}
|
|
|
|
return osValues.map((os) => {
|
|
// Map to standard Registry values (case-sensitive)
|
|
if (os === 'Windows' || os.toLowerCase().includes('win')) {
|
|
return 'Windows'
|
|
}
|
|
if (os === 'macOS' || os.toLowerCase().includes('mac') || os === 'darwin') {
|
|
return 'macOS'
|
|
}
|
|
if (os === 'Linux' || os.toLowerCase().includes('linux')) {
|
|
return 'Linux'
|
|
}
|
|
if (os.toLowerCase() === 'any') {
|
|
return 'any'
|
|
}
|
|
|
|
// Return as-is if it matches standard format
|
|
return os
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Detects operating system from system stats OS string and additional system information.
|
|
* @param systemOS OS string from system stats API
|
|
* @param systemStats Full system stats object for additional context
|
|
* @returns Operating system type
|
|
*/
|
|
function detectOSFromSystemStats(
|
|
systemOS: string,
|
|
systemStats?: SystemStats | null
|
|
): string {
|
|
const os = systemOS.toLowerCase()
|
|
|
|
// Handle specific OS strings (return Registry standard format)
|
|
if (os.includes('darwin') || os.includes('mac')) return 'macOS'
|
|
if (os.includes('linux')) return 'Linux'
|
|
if (os.includes('win') || os === 'nt') return 'Windows'
|
|
|
|
// Handle Python's os.name values
|
|
if (os === 'posix') {
|
|
// posix could be macOS or Linux, need additional detection
|
|
|
|
// Method 1: Check for MPS device (Metal Performance Shaders = macOS)
|
|
if (systemStats?.devices) {
|
|
const hasMpsDevice = systemStats.devices.some(
|
|
(device) => device.type === 'mps'
|
|
)
|
|
if (hasMpsDevice) {
|
|
return 'macOS' // Registry standard format
|
|
}
|
|
}
|
|
|
|
// Method 2: Check user agent as fallback
|
|
const userAgent = navigator.userAgent.toLowerCase()
|
|
if (userAgent.includes('mac')) return 'macOS'
|
|
if (userAgent.includes('linux')) return 'Linux'
|
|
|
|
// Default to 'any' if we can't determine
|
|
return 'any'
|
|
}
|
|
|
|
return 'any'
|
|
}
|
|
|
|
/**
|
|
* Extracts architecture information from system stats.
|
|
* @param systemStats System stats data from API
|
|
* @returns Architecture string
|
|
*/
|
|
function extractArchitectureFromSystemStats(
|
|
systemStats: SystemStats | null
|
|
): string {
|
|
try {
|
|
if (systemStats?.devices && systemStats.devices.length > 0) {
|
|
// Check if we have MPS device (indicates Apple Silicon)
|
|
const hasMpsDevice = systemStats.devices.some(
|
|
(device) => device.type === 'mps'
|
|
)
|
|
|
|
if (hasMpsDevice) {
|
|
// MPS is only available on Apple Silicon Macs
|
|
return 'arm64'
|
|
}
|
|
|
|
// Check device names for architecture hints (fallback)
|
|
for (const device of systemStats.devices) {
|
|
if (!device?.name || typeof device.name !== 'string') {
|
|
continue
|
|
}
|
|
|
|
const deviceName = device.name.toLowerCase()
|
|
|
|
// Apple Silicon detection
|
|
if (
|
|
deviceName.includes('apple m1') ||
|
|
deviceName.includes('apple m2') ||
|
|
deviceName.includes('apple m3') ||
|
|
deviceName.includes('apple m4')
|
|
) {
|
|
return 'arm64'
|
|
}
|
|
|
|
// Intel/AMD detection
|
|
if (
|
|
deviceName.includes('intel') ||
|
|
deviceName.includes('amd') ||
|
|
deviceName.includes('nvidia') ||
|
|
deviceName.includes('geforce') ||
|
|
deviceName.includes('radeon')
|
|
) {
|
|
return 'x64'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to basic User-Agent detection if system stats don't provide clear info
|
|
return getArchitecture()
|
|
} catch (error) {
|
|
console.warn(
|
|
'[ConflictDetection] Failed to extract architecture from system stats:',
|
|
error
|
|
)
|
|
return getArchitecture()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts accelerator information from system stats.
|
|
* @param systemStats System stats data from store
|
|
* @returns Accelerator information object
|
|
*/
|
|
function extractAcceleratorInfo(systemStats: SystemStats | null): {
|
|
available: Node['supported_accelerators']
|
|
primary: string
|
|
memory_mb?: number
|
|
} {
|
|
try {
|
|
if (systemStats?.devices && systemStats.devices.length > 0) {
|
|
const accelerators = new Set<string>()
|
|
let primaryDevice: string = 'CPU'
|
|
let totalMemory = 0
|
|
let maxDevicePriority = 0
|
|
|
|
// Device type priority (higher = better)
|
|
const getDevicePriority = (type: string): number => {
|
|
switch (type.toLowerCase()) {
|
|
case 'cuda':
|
|
return 5
|
|
case 'mps':
|
|
return 4
|
|
case 'rocm':
|
|
return 3
|
|
case 'xpu':
|
|
return 2 // Intel GPU
|
|
case 'npu':
|
|
return 1 // Neural Processing Unit
|
|
case 'mlu':
|
|
return 1 // Cambricon MLU
|
|
case 'cpu':
|
|
return 0
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Process all devices
|
|
for (const device of systemStats.devices) {
|
|
const deviceType = device.type.toLowerCase()
|
|
const priority = getDevicePriority(deviceType)
|
|
|
|
// Map device type to SupportedAccelerator (Registry standard format)
|
|
let acceleratorType: string = 'CPU'
|
|
if (deviceType === 'cuda') {
|
|
acceleratorType = 'CUDA'
|
|
} else if (deviceType === 'mps') {
|
|
acceleratorType = 'Metal' // MPS = Metal Performance Shaders
|
|
} else if (deviceType === 'rocm') {
|
|
acceleratorType = 'ROCm'
|
|
}
|
|
|
|
accelerators.add(acceleratorType)
|
|
|
|
// Update primary device if this one has higher priority
|
|
if (priority > maxDevicePriority) {
|
|
primaryDevice = acceleratorType
|
|
maxDevicePriority = priority
|
|
}
|
|
|
|
// Accumulate memory from all devices
|
|
if (device.vram_total) {
|
|
totalMemory += device.vram_total
|
|
}
|
|
}
|
|
|
|
accelerators.add('CPU') // CPU is always available
|
|
|
|
return {
|
|
available: Array.from(accelerators),
|
|
primary: primaryDevice,
|
|
memory_mb:
|
|
totalMemory > 0 ? Math.round(totalMemory / 1024 / 1024) : undefined
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
'[ConflictDetection] Failed to extract GPU information:',
|
|
error
|
|
)
|
|
}
|
|
|
|
// Default values
|
|
return {
|
|
available: ['CPU'],
|
|
primary: 'CPU',
|
|
memory_mb: undefined
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unified version conflict check using Registry API version strings.
|
|
* Uses shared versionUtil functions for consistent version handling.
|
|
* @param type Type of version being checked
|
|
* @param currentVersion Current version string
|
|
* @param supportedVersion Supported version from Registry
|
|
* @returns Conflict detail if conflict exists, null otherwise
|
|
*/
|
|
function checkVersionConflict(
|
|
type: ConflictType,
|
|
currentVersion: string,
|
|
supportedVersion: string
|
|
): ConflictDetail | null {
|
|
// If current version is unknown, assume compatible (no conflict)
|
|
if (currentVersion === 'unknown') {
|
|
return null
|
|
}
|
|
|
|
// If Registry doesn't specify version requirements, assume compatible
|
|
if (!supportedVersion || supportedVersion.trim() === '') {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
// Clean the current version using shared utility
|
|
const cleanCurrent = cleanVersion(currentVersion)
|
|
|
|
// Check version compatibility using shared utility
|
|
const isCompatible = satisfiesVersion(cleanCurrent, supportedVersion)
|
|
|
|
if (!isCompatible) {
|
|
return {
|
|
type,
|
|
current_value: currentVersion,
|
|
required_value: supportedVersion
|
|
}
|
|
}
|
|
|
|
return null
|
|
} catch (error) {
|
|
console.warn(
|
|
`[ConflictDetection] Failed to parse version requirement: ${supportedVersion}`,
|
|
error
|
|
)
|
|
return {
|
|
type,
|
|
current_value: currentVersion,
|
|
required_value: supportedVersion
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks for OS compatibility conflicts.
|
|
*/
|
|
function checkOSConflict(
|
|
supportedOS: Node['supported_os'],
|
|
currentOS: string
|
|
): ConflictDetail | null {
|
|
if (supportedOS?.includes('any') || supportedOS?.includes(currentOS)) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
type: 'os',
|
|
current_value: currentOS,
|
|
required_value: supportedOS ? supportedOS?.join(', ') : ''
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks for accelerator compatibility conflicts.
|
|
*/
|
|
function checkAcceleratorConflict(
|
|
supportedAccelerators: Node['supported_accelerators'],
|
|
availableAccelerators: Node['supported_accelerators']
|
|
): ConflictDetail | null {
|
|
if (
|
|
supportedAccelerators?.includes('any') ||
|
|
supportedAccelerators?.some((acc) => availableAccelerators?.includes(acc))
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
type: 'accelerator',
|
|
current_value: availableAccelerators
|
|
? availableAccelerators.join(', ')
|
|
: '',
|
|
required_value: supportedAccelerators
|
|
? supportedAccelerators.join(', ')
|
|
: ''
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks for banned package status conflicts.
|
|
*/
|
|
function checkBannedStatus(isBanned?: boolean): ConflictDetail | null {
|
|
if (isBanned === true) {
|
|
return {
|
|
type: 'banned',
|
|
current_value: 'installed',
|
|
required_value: 'not_banned'
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Checks for pending package status conflicts.
|
|
*/
|
|
function checkPendingStatus(isPending?: boolean): ConflictDetail | null {
|
|
if (isPending === true) {
|
|
return {
|
|
type: 'pending',
|
|
current_value: 'installed',
|
|
required_value: 'not_pending'
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Generates summary of conflict detection results.
|
|
*/
|
|
function generateSummary(
|
|
results: ConflictDetectionResult[],
|
|
durationMs: number
|
|
): ConflictDetectionSummary {
|
|
const conflictsByType: Record<ConflictType, number> = {
|
|
comfyui_version: 0,
|
|
frontend_version: 0,
|
|
import_failed: 0,
|
|
os: 0,
|
|
accelerator: 0,
|
|
banned: 0,
|
|
pending: 0
|
|
// python_version: 0
|
|
}
|
|
|
|
const conflictsByTypeDetails: Record<ConflictType, string[]> = {
|
|
comfyui_version: [],
|
|
frontend_version: [],
|
|
import_failed: [],
|
|
os: [],
|
|
accelerator: [],
|
|
banned: [],
|
|
pending: []
|
|
// python_version: [],
|
|
}
|
|
|
|
let bannedCount = 0
|
|
let securityPendingCount = 0
|
|
|
|
results.forEach((result) => {
|
|
result.conflicts.forEach((conflict) => {
|
|
conflictsByType[conflict.type]++
|
|
|
|
if (!conflictsByTypeDetails[conflict.type].includes(result.package_id)) {
|
|
conflictsByTypeDetails[conflict.type].push(result.package_id)
|
|
}
|
|
|
|
if (conflict.type === 'banned') bannedCount++
|
|
if (conflict.type === 'pending') securityPendingCount++
|
|
})
|
|
})
|
|
|
|
return {
|
|
total_packages: results.length,
|
|
compatible_packages: results.filter((r) => r.is_compatible).length,
|
|
conflicted_packages: results.filter((r) => r.has_conflict).length,
|
|
banned_packages: bannedCount,
|
|
pending_packages: securityPendingCount,
|
|
conflicts_by_type_details: conflictsByTypeDetails,
|
|
last_check_timestamp: new Date().toISOString(),
|
|
check_duration_ms: durationMs
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an empty summary for error cases.
|
|
*/
|
|
function getEmptySummary(): ConflictDetectionSummary {
|
|
return {
|
|
total_packages: 0,
|
|
compatible_packages: 0,
|
|
conflicted_packages: 0,
|
|
banned_packages: 0,
|
|
pending_packages: 0,
|
|
conflicts_by_type_details: {
|
|
comfyui_version: [],
|
|
frontend_version: [],
|
|
import_failed: [],
|
|
os: [],
|
|
accelerator: [],
|
|
banned: [],
|
|
pending: []
|
|
// python_version: [],
|
|
},
|
|
last_check_timestamp: new Date().toISOString(),
|
|
check_duration_ms: 0
|
|
}
|
|
}
|