Files
ComfyUI_frontend/src/composables/useConflictDetection.ts
Christian Byrne 66a76c0ee0 Upstream ComfyUI Manager frontend and add custom node conflict detection (#5291)
* migrate manager menu items

* Update locales [skip ci]

* switch to v2 manager API endpoints

* re-arrange menu items

* await promises. update settings schema

* move legacy option to startup arg

* Add banner indicating how to use legacy manager UI

* Update locales [skip ci]

* add "Check for Updates", "Install Missing" menu items

* Update locales [skip ci]

* use correct response shape

* improve command names

* dont show missing nodes button in legacy manager mode

* [Update to v2 API] update WS done message

* Update locales [skip ci]

* [fix] Fix json syntax error from rebase (#4607)

* Fix errors from rebase (removed `Tag` component import and duplicated imports in api.ts) (#4608)

Co-authored-by: github-actions <github-actions@github.com>

* Update locales [skip ci]

* [Manager] "Restarting" state after clicking restart button (#4637)

* [feat] Add reactive feature flags foundation (#4817)

* [feat] Add v2/ prefix to manager service base URL (#4872)

* [cleanup] Remove unused manager route enums (#4875)

* fix: v2 prefix (#5145)

* Fix: Restore api.ts from main branch after incorrect rebase (#5150)

* fix: api.ts file is different with main branch

* Update locales [skip ci]

* fix: restore support dotprop access

* fix: apply locales based on manager/menu-items-migration

* fix: Add missing shortcuts translation section for CI tests

- Added shortcuts section with keyboardShortcuts key
- Fixes failing Playwright test looking for 'Keyboard Shortcuts' aria-label
- Issue was caused by incomplete rebase from main branch

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Add missing versionMismatchWarning translations for CI tests

- Added versionMismatchWarning section with all required keys
- Added general versionMismatch related keys (updateFrontend, dismiss, etc.)
- Fixes failing Playwright tests for version mismatch warnings
- These keys were lost during the rebase from main branch

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Claude <noreply@anthropic.com>

* feat: Add loading state to PackInstallButton and improve UI (#5153)

* [restore] conflict notification commits restore

* [fix] Restore conflict notification work and fix tests

- Fix missing footerProps property in DialogInstance interface
- Add missing InstalledPacksResponse type import in tests
- Add missing getImportFailInfoBulk method to test mock
- Remove unused ManagerComponents import causing type error
- All unit and component tests now pass successfully

* [fix] Use Vue 3.5 destructuring syntax for props with defaults

Remove deprecated withDefaults usage in NodeConflictDialogContent.vue and use destructuring with default values instead

* [feature] dual modal supported

* [fix] Fix date format in PackCard test for locale consistency

* [fix] title text modified

* [fix] Fix conflict red dot not syncing
  between components

  Resolve reactivity issue by sharing
  useStorage refs across all
  composable instances to ensure UI
  consistency.

* [fix] Add conflict detection when installed packages list updates

- Import useConflictDetection composable in comfyManagerStore
- Call performConflictDetection after refreshing installed packages list
- Ensures conflict status stays up-to-date when packages change
- Follows existing codebase patterns for composable usage

* fix: use selected target_branch for PR base in update-manager-types workflow

* [fix]  test code timeout error fixed

* [chore] Update ComfyUI-Manager API types from ComfyUI-Manager@4e6f970 (#4782)

Co-authored-by: viva-jinyi <53567196+viva-jinyi@users.noreply.github.com>

* [types] Add proper types for ImportFailInfo API endpoints (#4783)

* [fix] ci error fixed & button max-width modified

* fix: node pack card width adapted

* fix: prevent duplicate api calls & installedPacksWithVersions instead of installpackids

* feat: run conflict detection after Apply Changes

Run performConflictDetection automatically after the backend restarts from Apply Changes button to detect conflicts in newly installed packages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: simplify PackInstallButton isInstalling state management

- Remove isInstalling prop from PackInstallButton component
- Use internal computed property with comfyManagerStore.isPackInstalling()
- Remove redundant isInstalling computations from parent components
- Fix test mocks for useConflictDetection and es-toolkit/compat
- Clean up unused imports and inject dependencies

This centralizes the installation state management in the store,
reducing code duplication and complexity across components.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: improve multi-package selection handling (#5116)

* feat: improve multi-package selection handling

- Check each package individually for conflicts in install dialog
- Show only packages with actual conflicts in warning dialog
- Hide action buttons for mixed installed/uninstalled selections
- Display dynamic status based on selected packages priority
- Deduplicate conflict information across multiple packages
- Fix PackIcon blur background opacity

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: extract multi-package logic into reusable composables

- Create usePackageSelection composable for installation state management
- Create usePackageStatus composable for status priority logic
- Refactor InfoPanelMultiItem to use new composables
- Reduce component complexity by separating business logic
- Improve code reusability across components

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: directory modified

* test: add comprehensive tests for multi-package selection composables

- Add tests for usePacksSelection composable
  - Test installation status filtering
  - Test selection state determination (all/none/mixed)
  - Test dynamic status changes

- Add tests for usePacksStatus composable
  - Test import failure detection
  - Test status priority handling
  - Test integration with conflict detection store

- Fix existing test mocking issues
  - Update es-toolkit/compat mock to use async import
  - Add Pinia setup for store-dependent tests
  - Update vue-i18n mock to preserve all exports

---------

Co-authored-by: Claude <noreply@anthropic.com>

* feat: Integrate ComfyUI Manager migration with v2 API and enhanced UI

This commit integrates the previously recovered ComfyUI Manager functionality
with significant enhancements from PR #3367, including:

## Core Manager System Recovery
- **v2 API Integration**: All manager endpoints now use `/v2/manager/queue/*`
- **Task Queue System**: Complete client-side task queuing with WebSocket status
- **Service Layer**: Comprehensive manager service with all CRUD operations
- **Store Integration**: Full manager store with progress dialog support

## New Features & Enhancements
- **Reactive Feature Flags**: Foundation for dynamic feature toggling
- **Enhanced UI Components**: Improved loading states, progress tracking
- **Package Management**: Install, update, enable/disable functionality
- **Version Selection**: Support for latest/nightly package versions
- **Progress Dialogs**: Real-time installation progress with logs
- **Missing Node Detection**: Automated detection and installation prompts

## Technical Improvements
- **TypeScript Definitions**: Complete type system for manager operations
- **WebSocket Integration**: Real-time status updates via `cm-queue-status`
- **Error Handling**: Comprehensive error handling with user feedback
- **Testing**: Updated test suites for new functionality
- **Documentation**: Complete backup documentation for recovery process

## API Endpoints Restored
- `manager/queue/start` - Start task queue
- `manager/queue/status` - Get queue status
- `manager/queue/task` - Queue individual tasks
- `manager/queue/install` - Install packages
- `manager/queue/update` - Update packages
- `manager/queue/disable` - Disable packages

## Breaking Changes
- Manager API base URL changed to `/v2/`
- Updated TypeScript interfaces for manager operations
- New WebSocket message format for queue status

This restores all critical manager functionality lost during the previous
rebase while integrating the latest enhancements and maintaining compatibility
with the current main branch.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Restore correct interfaces from PR #3367

- Restore original useManagerQueue, useServerLogs, and comfyManagerService interfaces
- Restore original component implementations for ManagerProgressDialogContent and ManagerProgressHeader
- Fix all TypeScript interface compatibility issues by using original PR implementations
- Remove duplicate setting that was causing runtime errors

This fixes merge errors where interfaces were incorrectly mixed between old and new implementations.

* fix: Add missing IconTextButton import in PackUninstallButton

Component was using IconTextButton in template but missing explicit import,
causing Vue runtime warning about unresolved component.

* docs: Update backup documentation with working state backup

Added manager-migration-clean-working-backup entry documenting the working state after fixing runtime issues, ready for PR integration.

* [feat] Add manager capability feature flags

Add support for manager v4 feature flag and client UI capability:
- MANAGER_SUPPORTS_V4: Server-side flag for v4 manager support
- supports_manager_v4_ui: Client-side flag for v4 UI support

These flags enable proper capability negotiation between frontend and
backend for manager UI selection (legacy vs v4).

Also fix TypeScript errors by adding @types/lodash.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [feat] Add managerStateStore for three-state manager UI logic

- Create managerStateStore to determine manager UI state (disabled, legacy, new)
- Check command line args, feature flags, and legacy API endpoints
- Update useCoreCommands to use the new store instead of async API calls
- Initialize manager state after system stats are loaded in GraphView
- Add comprehensive tests for all manager state scenarios

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [fix] Fix API URL prefix slash and add error handling

- Update comfyManagerService to use conditional API URL prefix based on manager v4 support
- Fix manager UI state handling in command menubar and workflow warning dialog
- Add proper manager state detection with fallback to settings panel
- Remove unused imports and variables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [docs] Update backup documentation with PR #5063 integration status

- Document manager-migration-pr5063-integrated backup branch
- Add comprehensive recovery verification for all integrated features
- Update next steps to reflect current progress
- Document successful integration of both PR #4654 and PR #5063

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [fix] Fix manager button visibility when manager is disabled

- Use managerStateStore instead of legacy isLegacyManager check
- Initialize manager state on component mount to detect --disable-manager
- Hide Install All Missing Custom Nodes button when manager is disabled
- Fixes issue where buttons showed even when comfyui_manager package not installed

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [fix] Correct Install All button visibility for manager UI states

- Install All Missing Custom Nodes button only shows for NEW_UI state
- Legacy UI state only shows Open Manager button
- Disabled state shows no buttons
- Matches original PR #5063 behavior exactly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Complete manager migration with bug fixes and locale updates

- Restore proper task queue implementation with generated types
- Fix manager button visibility based on server feature flags
- Add task completion tracking with taskIdToPackId mapping
- Fix log separation with task-specific filtering
- Implement failed tab functionality with proper task partitioning
- Fix task progress status detection using actual queue state
- Add missing locale entries for all manager operations
- Remove legacy manager menu items, keep only 'Manage Extensions'
- Fix task panel expansion state and count display issues
- All TypeScript and ESLint checks pass

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Complete manager migration with conflict detection integration

This completes the integration of ComfyUI Manager migration features with enhanced conflict detection system. Key changes include:

## Manager Migration & Conflict Detection
- Integrated PR #4637 (4-state manager restart workflow) with PR #4654 (comprehensive conflict detection)
- Fixed conflict detection to properly check `latest_version` fields for registry API compatibility
- Added conflict detection to PackCardFooter and InfoPanelHeader for comprehensive warning coverage
- Merged missing English locale translations from main branch with proper conflict resolution

## Bug Fixes
- Fixed double API path issue (`/api/v2/v2/`) in manager service routes
- Corrected PackUpdateButton payload structure and service method calls
- Enhanced conflict detection system to handle both installed and registry package structures

## Technical Improvements
- Updated conflict detection composable to handle both installed and registry package structures
- Enhanced manager service with proper error handling and route corrections
- Improved type safety across manager components with proper TypeScript definitions

* Remove temporary error log files from commits

* Remove temporary documentation files

- Remove MANAGER_MIGRATION_BACKUPS.md (temporary notes)
- Remove TASK_QUEUE_RESTORATION_PLAN.md (temporary notes)

These were development artifacts and shouldn't be in commits.

* feat: Complete manager migration cleanup and integration

- Remove outdated legacy manager detection from LoadWorkflowWarning
- Update InfoPanelHeader with conflict detection improvements
- Fix all failing unit tests from state management transition
- Clean up algolia search provider type mappings
- Remove unused @ts-expect-error directives
- Add .nx to .gitignore

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Update CustomNodesManager command to use tri-state manager system

Replace legacy isLegacyManagerUI() call with new ManagerUIState system:
- Use useManagerStateStore().managerUIState instead of async API call
- Handle DISABLED state by showing settings dialog
- Handle LEGACY_UI state with fallback to new UI on error
- Handle NEW_UI state by showing manager dialog
- Remove unused useComfyManagerService import

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Remove no-op refreshTaskState function

- Remove unused refreshTaskState function from useManagerQueue
- Function was left as no-op only to make tests pass
- Since queue is now push-based (WebSocket), no need to refresh state
- Clean up export and remove extra blank lines

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Replace lodash with es-toolkit/compat in useManagerQueue

Replace lodash import with es-toolkit/compat to match project standards:
- Change 'lodash' import to 'es-toolkit/compat' for pickBy function
- Add specific type helper for history task filtering
- Update JSDoc comment to remove lodash reference
- Fixes component test failures from missing lodash dependency

* fix: Add missing whats-new-dismissed event emission in WhatsNewPopup

During merge with main, the event emission was lost from the hide() function.
- Add defineEmits for 'whats-new-dismissed' event
- Emit event in hide() function to maintain test compatibility
- Fixes 3 failing unit tests in WhatsNewPopup.test.ts

* ci: Force CI run for Playwright tests

Previous commits contained [skip ci] which prevented test execution.
This empty commit ensures all CI checks run properly.

* test: Temporarily disable workflow.avif test due to missing nodes dialog

The workflow.avif test asset contains custom nodes that trigger the missing
nodes dialog, which is outside the scope of AVIF loading functionality testing.

TODO: Update test asset to use core nodes only, then re-enable the test.

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Jin Yi <jin12cc@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: viva-jinyi <53567196+viva-jinyi@users.noreply.github.com>
2025-09-02 19:14:36 -07:00

1360 lines
42 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
// First try latest_version (most accurate), then fallback to top level
const supportedOS =
('latest_version' in node ? node.latest_version?.supported_os : null) ||
node.supported_os
if (supportedOS && supportedOS.length > 0) {
const currentOS = systemStats.system?.os || 'unknown'
const osConflict = checkOSConflict(supportedOS, currentOS)
if (osConflict) {
conflicts.push(osConflict)
}
}
// Check accelerator compatibility using centralized function
// First try latest_version (most accurate), then fallback to top level
const supportedAccelerators =
('latest_version' in node
? node.latest_version?.supported_accelerators
: null) || node.supported_accelerators
if (supportedAccelerators && supportedAccelerators.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(
supportedAccelerators,
availableAccelerators
)
if (acceleratorConflict) {
conflicts.push(acceleratorConflict)
}
}
// Check ComfyUI version compatibility
// First try latest_version (most accurate), then fallback to top level
const comfyuiVersionRequirement =
('latest_version' in node
? node.latest_version?.supported_comfyui_version
: null) || node.supported_comfyui_version
if (comfyuiVersionRequirement) {
const currentComfyUIVersion = systemStats.system?.comfyui_version
if (currentComfyUIVersion && currentComfyUIVersion !== 'unknown') {
const versionConflict = utilCheckVersionCompatibility(
'comfyui_version',
currentComfyUIVersion,
comfyuiVersionRequirement
)
if (versionConflict) {
conflicts.push(versionConflict)
}
}
}
// Check ComfyUI Frontend version compatibility
// First try latest_version (most accurate), then fallback to top level
const frontendVersionRequirement =
('latest_version' in node
? node.latest_version?.supported_comfyui_frontend_version
: null) || node.supported_comfyui_frontend_version
if (frontendVersionRequirement) {
const currentFrontendVersion = config.app_version
if (currentFrontendVersion && currentFrontendVersion !== 'unknown') {
const versionConflict = utilCheckVersionCompatibility(
'frontend_version',
currentFrontendVersion,
frontendVersionRequirement
)
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
}
}