mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 16:24:06 +00:00
[Manager] Compatibility Detection Logic (#4348)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,13 @@ import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const conflictDetection = useConflictDetection()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
@@ -47,5 +49,9 @@ onMounted(() => {
|
||||
if (isElectron()) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
}
|
||||
|
||||
// Initialize conflict detection in background
|
||||
// This runs async and doesn't block UI setup
|
||||
void conflictDetection.initializeConflictDetection()
|
||||
})
|
||||
</script>
|
||||
|
||||
1096
src/composables/useConflictDetection.ts
Normal file
1096
src/composables/useConflictDetection.ts
Normal file
File diff suppressed because it is too large
Load Diff
239
src/stores/nodeCompatibilityStore.ts
Normal file
239
src/stores/nodeCompatibilityStore.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
ConflictType,
|
||||
SystemEnvironment
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
|
||||
interface IncompatibleNodeInfo {
|
||||
nodeId: string
|
||||
nodeName: string
|
||||
disableReason: ConflictType
|
||||
conflictDetails: string
|
||||
detectedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Store for managing node compatibility checking functionality.
|
||||
* Follows error-resilient patterns from useConflictDetection composable.
|
||||
*/
|
||||
export const useNodeCompatibilityStore = defineStore(
|
||||
'nodeCompatibility',
|
||||
() => {
|
||||
// Core state
|
||||
const isChecking = ref(false)
|
||||
const lastCheckTime = ref<string | null>(null)
|
||||
const checkError = ref<string | null>(null)
|
||||
const systemEnvironment = ref<SystemEnvironment | null>(null)
|
||||
|
||||
// Node tracking maps
|
||||
const incompatibleNodes = ref<Map<string, IncompatibleNodeInfo>>(new Map())
|
||||
const failedImportNodes = ref<Set<string>>(new Set())
|
||||
const bannedNodes = ref<Set<string>>(new Set())
|
||||
const securityPendingNodes = ref<Set<string>>(new Set())
|
||||
|
||||
// User interaction state
|
||||
const hasShownNotificationModal = ref(false)
|
||||
const pendingNotificationNodes = ref<IncompatibleNodeInfo[]>([])
|
||||
|
||||
// Computed properties
|
||||
const hasIncompatibleNodes = computed(
|
||||
() => incompatibleNodes.value.size > 0
|
||||
)
|
||||
const totalIncompatibleCount = computed(
|
||||
() =>
|
||||
incompatibleNodes.value.size +
|
||||
failedImportNodes.value.size +
|
||||
bannedNodes.value.size
|
||||
)
|
||||
|
||||
const incompatibleNodesList = computed(() =>
|
||||
Array.from(incompatibleNodes.value.values())
|
||||
)
|
||||
|
||||
const shouldShowNotification = computed(() => {
|
||||
// Show notification if there are incompatible nodes and we haven't shown notification yet
|
||||
return hasIncompatibleNodes.value && !hasShownNotificationModal.value
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if a node has compatibility issues.
|
||||
*/
|
||||
function hasNodeCompatibilityIssues(nodeId: string): boolean {
|
||||
return (
|
||||
incompatibleNodes.value.has(nodeId) ||
|
||||
failedImportNodes.value.has(nodeId) ||
|
||||
bannedNodes.value.has(nodeId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compatibility info for a node.
|
||||
*/
|
||||
function getNodeCompatibilityInfo(
|
||||
nodeId: string
|
||||
): IncompatibleNodeInfo | null {
|
||||
return incompatibleNodes.value.get(nodeId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node to the incompatible list.
|
||||
*/
|
||||
function addIncompatibleNode(
|
||||
nodeId: string,
|
||||
nodeName: string,
|
||||
reason: ConflictType,
|
||||
details: string
|
||||
): void {
|
||||
const info: IncompatibleNodeInfo = {
|
||||
nodeId,
|
||||
nodeName,
|
||||
disableReason: reason,
|
||||
conflictDetails: details,
|
||||
detectedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
incompatibleNodes.value.set(nodeId, info)
|
||||
|
||||
// Add to pending list (for notification purposes)
|
||||
if (!hasShownNotificationModal.value) {
|
||||
pendingNotificationNodes.value.push(info)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a node from the incompatible list.
|
||||
*/
|
||||
function removeIncompatibleNode(nodeId: string): void {
|
||||
incompatibleNodes.value.delete(nodeId)
|
||||
failedImportNodes.value.delete(nodeId)
|
||||
bannedNodes.value.delete(nodeId)
|
||||
securityPendingNodes.value.delete(nodeId)
|
||||
|
||||
// Remove from pending list
|
||||
pendingNotificationNodes.value = pendingNotificationNodes.value.filter(
|
||||
(node) => node.nodeId !== nodeId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all compatibility check results.
|
||||
*/
|
||||
function clearResults(): void {
|
||||
incompatibleNodes.value.clear()
|
||||
failedImportNodes.value.clear()
|
||||
bannedNodes.value.clear()
|
||||
securityPendingNodes.value.clear()
|
||||
pendingNotificationNodes.value = []
|
||||
checkError.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks that the notification modal has been shown.
|
||||
*/
|
||||
function markNotificationModalShown(): void {
|
||||
hasShownNotificationModal.value = true
|
||||
pendingNotificationNodes.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the notification modal state (for testing or re-initialization).
|
||||
*/
|
||||
function resetNotificationModalState(): void {
|
||||
hasShownNotificationModal.value = false
|
||||
pendingNotificationNodes.value = Array.from(
|
||||
incompatibleNodes.value.values()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the system environment information.
|
||||
*/
|
||||
function setSystemEnvironment(env: SystemEnvironment): void {
|
||||
systemEnvironment.value = env
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the checking state.
|
||||
*/
|
||||
function setCheckingState(checking: boolean): void {
|
||||
isChecking.value = checking
|
||||
if (checking) {
|
||||
checkError.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a successful check completion.
|
||||
*/
|
||||
function recordCheckCompletion(): void {
|
||||
lastCheckTime.value = new Date().toISOString()
|
||||
isChecking.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a check error.
|
||||
*/
|
||||
function recordCheckError(error: string): void {
|
||||
checkError.value = error
|
||||
isChecking.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a summary of the current compatibility state.
|
||||
*/
|
||||
function getCompatibilitySummary() {
|
||||
return {
|
||||
totalChecked: lastCheckTime.value ? 'completed' : 'pending',
|
||||
incompatibleCount: incompatibleNodes.value.size,
|
||||
failedImportCount: failedImportNodes.value.size,
|
||||
bannedCount: bannedNodes.value.size,
|
||||
securityPendingCount: securityPendingNodes.value.size,
|
||||
totalIssues: totalIncompatibleCount.value,
|
||||
lastCheckTime: lastCheckTime.value,
|
||||
hasError: !!checkError.value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isChecking: computed(() => isChecking.value),
|
||||
lastCheckTime: computed(() => lastCheckTime.value),
|
||||
checkError: computed(() => checkError.value),
|
||||
systemEnvironment: computed(() => systemEnvironment.value),
|
||||
|
||||
// Node tracking
|
||||
incompatibleNodes: computed(() => incompatibleNodes.value),
|
||||
incompatibleNodesList,
|
||||
failedImportNodes: computed(() => failedImportNodes.value),
|
||||
bannedNodes: computed(() => bannedNodes.value),
|
||||
securityPendingNodes: computed(() => securityPendingNodes.value),
|
||||
|
||||
// User interaction
|
||||
hasShownNotificationModal: computed(
|
||||
() => hasShownNotificationModal.value
|
||||
),
|
||||
pendingNotificationNodes: computed(() => pendingNotificationNodes.value),
|
||||
shouldShowNotification,
|
||||
|
||||
// Computed
|
||||
hasIncompatibleNodes,
|
||||
totalIncompatibleCount,
|
||||
|
||||
// Methods
|
||||
hasNodeCompatibilityIssues,
|
||||
getNodeCompatibilityInfo,
|
||||
addIncompatibleNode,
|
||||
removeIncompatibleNode,
|
||||
clearResults,
|
||||
markNotificationModalShown,
|
||||
resetNotificationModalState,
|
||||
setSystemEnvironment,
|
||||
setCheckingState,
|
||||
recordCheckCompletion,
|
||||
recordCheckError,
|
||||
getCompatibilitySummary
|
||||
}
|
||||
}
|
||||
)
|
||||
264
src/types/conflictDetectionTypes.ts
Normal file
264
src/types/conflictDetectionTypes.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Type definitions for the conflict detection system.
|
||||
* These types are used to detect compatibility issues between Node Packs and the system environment.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Conflict types that can be detected in the system
|
||||
* @enum {string}
|
||||
*/
|
||||
export type ConflictType =
|
||||
| 'comfyui_version' // ComfyUI version mismatch
|
||||
| 'frontend_version' // Frontend version mismatch
|
||||
| 'python_version' // Python version mismatch
|
||||
| 'os' // Operating system incompatibility
|
||||
| 'accelerator' // GPU/accelerator incompatibility
|
||||
| 'banned' // Banned package
|
||||
| 'security_pending' // Security verification pending
|
||||
|
||||
/**
|
||||
* Security scan status for packages
|
||||
* @enum {string}
|
||||
*/
|
||||
export type SecurityScanStatus = 'pending' | 'passed' | 'failed' | 'unknown'
|
||||
|
||||
/**
|
||||
* Supported operating systems (as per Registry Admin guide)
|
||||
* @enum {string}
|
||||
*/
|
||||
export type SupportedOS = 'Windows' | 'macOS' | 'Linux' | 'any'
|
||||
|
||||
/**
|
||||
* Supported accelerators for GPU computation (as per Registry Admin guide)
|
||||
* @enum {string}
|
||||
*/
|
||||
export type SupportedAccelerator = 'CUDA' | 'ROCm' | 'Metal' | 'CPU' | 'any'
|
||||
|
||||
/**
|
||||
* Version comparison operators
|
||||
* @enum {string}
|
||||
*/
|
||||
export type VersionOperator = '>=' | '>' | '<=' | '<' | '==' | '!='
|
||||
|
||||
/**
|
||||
* Version requirement specification
|
||||
*/
|
||||
export interface VersionRequirement {
|
||||
/** @description Comparison operator for version checking */
|
||||
operator: VersionOperator
|
||||
/** @description Target version string */
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Node Pack requirements from Registry API
|
||||
*/
|
||||
export interface NodePackRequirements {
|
||||
/** @description Unique package identifier */
|
||||
package_id: string
|
||||
/** @description Human-readable package name */
|
||||
package_name: string
|
||||
/** @description Currently installed version */
|
||||
installed_version: string
|
||||
/** @description Whether the package is enabled locally */
|
||||
is_enabled: boolean
|
||||
|
||||
/** @description Supported ComfyUI version from Registry */
|
||||
supported_comfyui_version?: string
|
||||
/** @description Supported frontend version from Registry */
|
||||
supported_comfyui_frontend_version?: string
|
||||
/** @description List of supported operating systems from Registry */
|
||||
supported_os?: SupportedOS[]
|
||||
/** @description List of supported accelerators from Registry */
|
||||
supported_accelerators?: SupportedAccelerator[]
|
||||
/** @description Package dependencies from Registry */
|
||||
dependencies?: string[]
|
||||
|
||||
/** @description Node status from Registry (Active/Banned/Deleted) */
|
||||
registry_status?:
|
||||
| 'NodeStatusActive'
|
||||
| 'NodeStatusBanned'
|
||||
| 'NodeStatusDeleted'
|
||||
/** @description Node version status from Registry */
|
||||
version_status?:
|
||||
| 'NodeVersionStatusActive'
|
||||
| 'NodeVersionStatusBanned'
|
||||
| 'NodeVersionStatusDeleted'
|
||||
| 'NodeVersionStatusPending'
|
||||
| 'NodeVersionStatusFlagged'
|
||||
/** @description Whether package is banned (derived from status) */
|
||||
is_banned: boolean
|
||||
/** @description Reason for ban if applicable */
|
||||
ban_reason?: string
|
||||
|
||||
// Metadata
|
||||
/** @description Registry data fetch timestamp */
|
||||
registry_fetch_time: string
|
||||
/** @description Whether Registry data was successfully fetched */
|
||||
has_registry_data: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Current system environment information
|
||||
*/
|
||||
export interface SystemEnvironment {
|
||||
// Version information
|
||||
/** @description Current ComfyUI version */
|
||||
comfyui_version: string
|
||||
/** @description Current frontend version */
|
||||
frontend_version: string
|
||||
/** @description Current Python version */
|
||||
python_version: string
|
||||
|
||||
// Platform information
|
||||
/** @description Operating system type */
|
||||
os: SupportedOS
|
||||
/** @description Detailed platform information (e.g., 'Darwin 24.5.0', 'Windows 10') */
|
||||
platform_details: string
|
||||
/** @description System architecture (e.g., 'x64', 'arm64') */
|
||||
architecture: string
|
||||
|
||||
// GPU/accelerator information
|
||||
/** @description List of available accelerators */
|
||||
available_accelerators: SupportedAccelerator[]
|
||||
/** @description Primary accelerator in use */
|
||||
primary_accelerator: SupportedAccelerator
|
||||
/** @description GPU memory in megabytes, if available */
|
||||
gpu_memory_mb?: number
|
||||
|
||||
// Runtime information
|
||||
/** @description Node.js environment mode */
|
||||
node_env: 'development' | 'production'
|
||||
/** @description Browser user agent string */
|
||||
user_agent: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual conflict detection result for a package
|
||||
*/
|
||||
export interface ConflictDetectionResult {
|
||||
/** @description Package identifier */
|
||||
package_id: string
|
||||
/** @description Package name */
|
||||
package_name: string
|
||||
/** @description Whether any conflicts were detected */
|
||||
has_conflict: boolean
|
||||
/** @description List of detected conflicts */
|
||||
conflicts: ConflictDetail[]
|
||||
/** @description Overall compatibility status */
|
||||
is_compatible: boolean
|
||||
/** @description Whether conflicts can be automatically resolved */
|
||||
can_auto_resolve: boolean
|
||||
/** @description Recommended action to resolve conflicts */
|
||||
recommended_action: RecommendedAction
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed information about a specific conflict
|
||||
*/
|
||||
export interface ConflictDetail {
|
||||
/** @description Type of conflict detected */
|
||||
type: ConflictType
|
||||
/** @description Severity level of the conflict */
|
||||
severity: 'error' | 'warning' | 'info'
|
||||
/** @description Human-readable description of the conflict */
|
||||
description: string
|
||||
/** @description Current system value */
|
||||
current_value: string
|
||||
/** @description Required value for compatibility */
|
||||
required_value: string
|
||||
/** @description Optional steps to resolve the conflict */
|
||||
resolution_steps?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommended action to resolve conflicts
|
||||
*/
|
||||
export interface RecommendedAction {
|
||||
/** @description Type of action to take */
|
||||
action_type: 'disable' | 'update' | 'ignore' | 'manual_review'
|
||||
/** @description Reason for the recommended action */
|
||||
reason: string
|
||||
/** @description Step-by-step instructions */
|
||||
steps: string[]
|
||||
/** @description Estimated difficulty of implementing the action */
|
||||
estimated_difficulty: 'easy' | 'medium' | 'hard'
|
||||
}
|
||||
|
||||
/**
|
||||
* Overall conflict detection summary
|
||||
*/
|
||||
export interface ConflictDetectionSummary {
|
||||
/** @description Total number of packages checked */
|
||||
total_packages: number
|
||||
/** @description Number of compatible packages */
|
||||
compatible_packages: number
|
||||
/** @description Number of packages with conflicts */
|
||||
conflicted_packages: number
|
||||
/** @description Number of banned packages */
|
||||
banned_packages: number
|
||||
/** @description Number of packages pending security verification */
|
||||
security_pending_packages: number
|
||||
/** @description Node IDs grouped by conflict type */
|
||||
conflicts_by_type_details: Record<ConflictType, string[]>
|
||||
/** @description Timestamp of the last conflict check */
|
||||
last_check_timestamp: string
|
||||
/** @description Duration of the conflict check in milliseconds */
|
||||
check_duration_ms: number
|
||||
}
|
||||
|
||||
/**
|
||||
* API request/response interfaces
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request payload for conflict detection API
|
||||
*/
|
||||
export interface ConflictDetectionRequest {
|
||||
/** @description Current system environment information */
|
||||
system_environment: SystemEnvironment
|
||||
/** @description Optional list of specific package IDs to check */
|
||||
package_ids?: string[]
|
||||
/** @description Whether to include banned packages in the check */
|
||||
include_banned?: boolean
|
||||
/** @description Whether to include security-pending packages in the check */
|
||||
include_security_pending?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Response payload from conflict detection API
|
||||
*/
|
||||
export interface ConflictDetectionResponse {
|
||||
/** @description Whether the API request was successful */
|
||||
success: boolean
|
||||
/** @description Error message if the request failed */
|
||||
error_message?: string
|
||||
|
||||
/** @description Summary of the conflict detection results */
|
||||
summary: ConflictDetectionSummary
|
||||
/** @description Detailed results for each package */
|
||||
results: ConflictDetectionResult[]
|
||||
|
||||
/** @description System environment information detected by the server (for comparison) */
|
||||
detected_system_environment?: Partial<SystemEnvironment>
|
||||
}
|
||||
|
||||
/**
|
||||
* Real-time conflict detection event
|
||||
*/
|
||||
export interface ConflictDetectionEvent {
|
||||
/** @description Type of event */
|
||||
event_type:
|
||||
| 'conflict_detected'
|
||||
| 'conflict_resolved'
|
||||
| 'scan_started'
|
||||
| 'scan_completed'
|
||||
/** @description Event timestamp */
|
||||
timestamp: string
|
||||
/** @description Package ID associated with the event, if applicable */
|
||||
package_id?: string
|
||||
/** @description Type of conflict, if applicable */
|
||||
conflict_type?: ConflictType
|
||||
/** @description Additional event details */
|
||||
details?: string
|
||||
}
|
||||
80
src/utils/versionUtil.ts
Normal file
80
src/utils/versionUtil.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as semver from 'semver'
|
||||
|
||||
/**
|
||||
* Cleans a version string by removing common prefixes and normalizing format
|
||||
* @param version Raw version string (e.g., "v1.2.3", "1.2.3-alpha")
|
||||
* @returns Cleaned version string or original if cleaning fails
|
||||
*/
|
||||
export function cleanVersion(version: string): string {
|
||||
return semver.clean(version) || version
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version satisfies a version range
|
||||
* @param version Current version
|
||||
* @param range Version range (e.g., ">=1.0.0", "^1.2.0", "1.0.0 - 2.0.0")
|
||||
* @returns true if version satisfies the range
|
||||
*/
|
||||
export function satisfiesVersion(version: string, range: string): boolean {
|
||||
try {
|
||||
const cleanedVersion = cleanVersion(version)
|
||||
return semver.satisfies(cleanedVersion, range)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two versions and returns the difference type
|
||||
* @param version1 First version
|
||||
* @param version2 Second version
|
||||
* @returns Difference type or null if comparison fails
|
||||
*/
|
||||
export function getVersionDifference(
|
||||
version1: string,
|
||||
version2: string
|
||||
): semver.ReleaseType | null {
|
||||
try {
|
||||
const clean1 = cleanVersion(version1)
|
||||
const clean2 = cleanVersion(version2)
|
||||
return semver.diff(clean1, clean2)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version is valid according to semver
|
||||
* @param version Version string to validate
|
||||
* @returns true if version is valid
|
||||
*/
|
||||
export function isValidVersion(version: string): boolean {
|
||||
return semver.valid(version) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a human-readable description of a version range
|
||||
* @param range Version range string
|
||||
* @returns Description of what the range means
|
||||
*/
|
||||
export function describeVersionRange(range: string): string {
|
||||
if (range.startsWith('>=')) {
|
||||
return `version ${range.substring(2)} or higher`
|
||||
} else if (range.startsWith('>')) {
|
||||
return `version higher than ${range.substring(1)}`
|
||||
} else if (range.startsWith('<=')) {
|
||||
return `version ${range.substring(2)} or lower`
|
||||
} else if (range.startsWith('<')) {
|
||||
return `version lower than ${range.substring(1)}`
|
||||
} else if (range.startsWith('^')) {
|
||||
return `compatible with version ${range.substring(1)}`
|
||||
} else if (range.startsWith('~')) {
|
||||
return `approximately version ${range.substring(1)}`
|
||||
} else if (range.includes(' - ')) {
|
||||
const [min, max] = range.split(' - ')
|
||||
return `version between ${min} and ${max}`
|
||||
} else if (range.includes('||')) {
|
||||
return `one of multiple version ranges: ${range}`
|
||||
}
|
||||
return `version ${range}`
|
||||
}
|
||||
Reference in New Issue
Block a user