[Manager] Compatibility Detection Logic (#4348)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-07-08 06:44:53 +09:00
parent 5055092cc5
commit 0a2f2d8368
9 changed files with 2726 additions and 121 deletions

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View 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
}
}
)

View 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
View 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}`
}