Add localAIManager
This commit is contained in:
		
							parent
							
								
									6ece74737b
								
							
						
					
					
						commit
						9359b32c01
					
				
							
								
								
									
										639
									
								
								src/features/common/services/localAIManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										639
									
								
								src/features/common/services/localAIManager.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,639 @@
 | 
				
			|||||||
 | 
					const { EventEmitter } = require('events');
 | 
				
			||||||
 | 
					const ollamaService = require('./ollamaService');
 | 
				
			||||||
 | 
					const whisperService = require('./whisperService');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//Central manager for managing Ollama and Whisper services 
 | 
				
			||||||
 | 
					class LocalAIManager extends EventEmitter {
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // service map
 | 
				
			||||||
 | 
					        this.services = {
 | 
				
			||||||
 | 
					            ollama: ollamaService,
 | 
				
			||||||
 | 
					            whisper: whisperService
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // unified state management
 | 
				
			||||||
 | 
					        this.state = {
 | 
				
			||||||
 | 
					            ollama: {
 | 
				
			||||||
 | 
					                installed: false,
 | 
				
			||||||
 | 
					                running: false,
 | 
				
			||||||
 | 
					                models: []
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            whisper: {
 | 
				
			||||||
 | 
					                installed: false,
 | 
				
			||||||
 | 
					                initialized: false,
 | 
				
			||||||
 | 
					                models: []
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // setup event listeners
 | 
				
			||||||
 | 
					        this.setupEventListeners();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // subscribe to events from each service and re-emit as unified events
 | 
				
			||||||
 | 
					    setupEventListeners() {
 | 
				
			||||||
 | 
					        // ollama events
 | 
				
			||||||
 | 
					        ollamaService.on('install-progress', (data) => {
 | 
				
			||||||
 | 
					            this.emit('install-progress', 'ollama', data);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        ollamaService.on('installation-complete', () => {
 | 
				
			||||||
 | 
					            this.emit('installation-complete', 'ollama');
 | 
				
			||||||
 | 
					            this.updateServiceState('ollama');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        ollamaService.on('error', (error) => {
 | 
				
			||||||
 | 
					            this.emit('error', { service: 'ollama', ...error });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        ollamaService.on('model-pull-complete', (data) => {
 | 
				
			||||||
 | 
					            this.emit('model-ready', { service: 'ollama', ...data });
 | 
				
			||||||
 | 
					            this.updateServiceState('ollama');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        ollamaService.on('state-changed', (state) => {
 | 
				
			||||||
 | 
					            this.emit('state-changed', 'ollama', state);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Whisper 이벤트
 | 
				
			||||||
 | 
					        whisperService.on('install-progress', (data) => {
 | 
				
			||||||
 | 
					            this.emit('install-progress', 'whisper', data);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        whisperService.on('installation-complete', () => {
 | 
				
			||||||
 | 
					            this.emit('installation-complete', 'whisper');
 | 
				
			||||||
 | 
					            this.updateServiceState('whisper');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        whisperService.on('error', (error) => {
 | 
				
			||||||
 | 
					            this.emit('error', { service: 'whisper', ...error });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        whisperService.on('model-download-complete', (data) => {
 | 
				
			||||||
 | 
					            this.emit('model-ready', { service: 'whisper', ...data });
 | 
				
			||||||
 | 
					            this.updateServiceState('whisper');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 서비스 설치
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async installService(serviceName, options = {}) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (serviceName === 'ollama') {
 | 
				
			||||||
 | 
					                return await service.handleInstall();
 | 
				
			||||||
 | 
					            } else if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					                // Whisper는 자동 설치
 | 
				
			||||||
 | 
					                await service.initialize();
 | 
				
			||||||
 | 
					                return { success: true };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            this.emit('error', {
 | 
				
			||||||
 | 
					                service: serviceName,
 | 
				
			||||||
 | 
					                errorType: 'installation-failed',
 | 
				
			||||||
 | 
					                error: error.message
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            throw error;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 서비스 상태 조회
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getServiceStatus(serviceName) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (serviceName === 'ollama') {
 | 
				
			||||||
 | 
					            return await service.getStatus();
 | 
				
			||||||
 | 
					        } else if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					            const installed = await service.isInstalled();
 | 
				
			||||||
 | 
					            const running = await service.isServiceRunning();
 | 
				
			||||||
 | 
					            const models = await service.getInstalledModels();
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                success: true,
 | 
				
			||||||
 | 
					                installed,
 | 
				
			||||||
 | 
					                running,
 | 
				
			||||||
 | 
					                models
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 서비스 시작
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async startService(serviceName) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const result = await service.startService();
 | 
				
			||||||
 | 
					        await this.updateServiceState(serviceName);
 | 
				
			||||||
 | 
					        return { success: result };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 서비스 중지
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async stopService(serviceName) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let result;
 | 
				
			||||||
 | 
					        if (serviceName === 'ollama') {
 | 
				
			||||||
 | 
					            result = await service.shutdown(false);
 | 
				
			||||||
 | 
					        } else if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					            result = await service.stopService();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 서비스 중지 후 상태 업데이트
 | 
				
			||||||
 | 
					        await this.updateServiceState(serviceName);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return result;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 모델 설치/다운로드
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async installModel(serviceName, modelId, options = {}) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (serviceName === 'ollama') {
 | 
				
			||||||
 | 
					            return await service.pullModel(modelId);
 | 
				
			||||||
 | 
					        } else if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					            return await service.downloadModel(modelId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 설치된 모델 목록 조회
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getInstalledModels(serviceName) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (serviceName === 'ollama') {
 | 
				
			||||||
 | 
					            return await service.getAllModelsWithStatus();
 | 
				
			||||||
 | 
					        } else if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					            return await service.getInstalledModels();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 모델 워밍업 (Ollama 전용)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async warmUpModel(modelName, forceRefresh = false) {
 | 
				
			||||||
 | 
					        return await ollamaService.warmUpModel(modelName, forceRefresh);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 자동 워밍업 (Ollama 전용)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async autoWarmUp() {
 | 
				
			||||||
 | 
					        return await ollamaService.autoWarmUpSelectedModel();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 진단 실행
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async runDiagnostics(serviceName) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const diagnostics = {
 | 
				
			||||||
 | 
					            service: serviceName,
 | 
				
			||||||
 | 
					            timestamp: new Date().toISOString(),
 | 
				
			||||||
 | 
					            checks: {}
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 1. 설치 상태 확인
 | 
				
			||||||
 | 
					            diagnostics.checks.installation = {
 | 
				
			||||||
 | 
					                check: 'Installation',
 | 
				
			||||||
 | 
					                status: await service.isInstalled() ? 'pass' : 'fail',
 | 
				
			||||||
 | 
					                details: {}
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 2. 서비스 실행 상태
 | 
				
			||||||
 | 
					            diagnostics.checks.running = {
 | 
				
			||||||
 | 
					                check: 'Service Running',
 | 
				
			||||||
 | 
					                status: await service.isServiceRunning() ? 'pass' : 'fail',
 | 
				
			||||||
 | 
					                details: {}
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 3. 포트 연결 테스트 및 상세 health check (Ollama)
 | 
				
			||||||
 | 
					            if (serviceName === 'ollama') {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    // Use comprehensive health check
 | 
				
			||||||
 | 
					                    const health = await service.healthCheck();
 | 
				
			||||||
 | 
					                    diagnostics.checks.health = {
 | 
				
			||||||
 | 
					                        check: 'Service Health',
 | 
				
			||||||
 | 
					                        status: health.healthy ? 'pass' : 'fail',
 | 
				
			||||||
 | 
					                        details: health
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Legacy port check for compatibility
 | 
				
			||||||
 | 
					                    diagnostics.checks.port = {
 | 
				
			||||||
 | 
					                        check: 'Port Connectivity',
 | 
				
			||||||
 | 
					                        status: health.checks.apiResponsive ? 'pass' : 'fail',
 | 
				
			||||||
 | 
					                        details: { connected: health.checks.apiResponsive }
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    diagnostics.checks.health = {
 | 
				
			||||||
 | 
					                        check: 'Service Health',
 | 
				
			||||||
 | 
					                        status: 'fail',
 | 
				
			||||||
 | 
					                        details: { error: error.message }
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    diagnostics.checks.port = {
 | 
				
			||||||
 | 
					                        check: 'Port Connectivity',
 | 
				
			||||||
 | 
					                        status: 'fail',
 | 
				
			||||||
 | 
					                        details: { error: error.message }
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 4. 모델 목록
 | 
				
			||||||
 | 
					                if (diagnostics.checks.running.status === 'pass') {
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        const models = await service.getInstalledModels();
 | 
				
			||||||
 | 
					                        diagnostics.checks.models = {
 | 
				
			||||||
 | 
					                            check: 'Installed Models',
 | 
				
			||||||
 | 
					                            status: 'pass',
 | 
				
			||||||
 | 
					                            details: { count: models.length, models: models.map(m => m.name) }
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // 5. 워밍업 상태
 | 
				
			||||||
 | 
					                        const warmupStatus = await service.getWarmUpStatus();
 | 
				
			||||||
 | 
					                        diagnostics.checks.warmup = {
 | 
				
			||||||
 | 
					                            check: 'Model Warm-up',
 | 
				
			||||||
 | 
					                            status: 'pass',
 | 
				
			||||||
 | 
					                            details: warmupStatus
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                    } catch (error) {
 | 
				
			||||||
 | 
					                        diagnostics.checks.models = {
 | 
				
			||||||
 | 
					                            check: 'Installed Models',
 | 
				
			||||||
 | 
					                            status: 'fail',
 | 
				
			||||||
 | 
					                            details: { error: error.message }
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 4. Whisper 특화 진단
 | 
				
			||||||
 | 
					            if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					                // 바이너리 확인
 | 
				
			||||||
 | 
					                diagnostics.checks.binary = {
 | 
				
			||||||
 | 
					                    check: 'Whisper Binary',
 | 
				
			||||||
 | 
					                    status: service.whisperPath ? 'pass' : 'fail',
 | 
				
			||||||
 | 
					                    details: { path: service.whisperPath }
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 모델 디렉토리
 | 
				
			||||||
 | 
					                diagnostics.checks.modelDir = {
 | 
				
			||||||
 | 
					                    check: 'Model Directory',
 | 
				
			||||||
 | 
					                    status: service.modelsDir ? 'pass' : 'fail',
 | 
				
			||||||
 | 
					                    details: { path: service.modelsDir }
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 전체 진단 결과
 | 
				
			||||||
 | 
					            const allChecks = Object.values(diagnostics.checks);
 | 
				
			||||||
 | 
					            diagnostics.summary = {
 | 
				
			||||||
 | 
					                total: allChecks.length,
 | 
				
			||||||
 | 
					                passed: allChecks.filter(c => c.status === 'pass').length,
 | 
				
			||||||
 | 
					                failed: allChecks.filter(c => c.status === 'fail').length,
 | 
				
			||||||
 | 
					                overallStatus: allChecks.every(c => c.status === 'pass') ? 'healthy' : 'unhealthy'
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            diagnostics.error = error.message;
 | 
				
			||||||
 | 
					            diagnostics.summary = {
 | 
				
			||||||
 | 
					                overallStatus: 'error'
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return diagnostics;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 서비스 복구
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async repairService(serviceName) {
 | 
				
			||||||
 | 
					        const service = this.services[serviceName];
 | 
				
			||||||
 | 
					        if (!service) {
 | 
				
			||||||
 | 
					            throw new Error(`Unknown service: ${serviceName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        console.log(`[LocalAIManager] Starting repair for ${serviceName}...`);
 | 
				
			||||||
 | 
					        const repairLog = [];
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 1. 진단 실행
 | 
				
			||||||
 | 
					            repairLog.push('Running diagnostics...');
 | 
				
			||||||
 | 
					            const diagnostics = await this.runDiagnostics(serviceName);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (diagnostics.summary.overallStatus === 'healthy') {
 | 
				
			||||||
 | 
					                repairLog.push('Service is already healthy, no repair needed');
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    success: true,
 | 
				
			||||||
 | 
					                    repairLog,
 | 
				
			||||||
 | 
					                    diagnostics
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 2. 설치 문제 해결
 | 
				
			||||||
 | 
					            if (diagnostics.checks.installation?.status === 'fail') {
 | 
				
			||||||
 | 
					                repairLog.push('Installation missing, attempting to install...');
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    await this.installService(serviceName);
 | 
				
			||||||
 | 
					                    repairLog.push('Installation completed');
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    repairLog.push(`Installation failed: ${error.message}`);
 | 
				
			||||||
 | 
					                    throw error;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 3. 서비스 재시작
 | 
				
			||||||
 | 
					            if (diagnostics.checks.running?.status === 'fail') {
 | 
				
			||||||
 | 
					                repairLog.push('Service not running, attempting to start...');
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 종료 시도
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    await this.stopService(serviceName);
 | 
				
			||||||
 | 
					                    repairLog.push('Stopped existing service');
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    repairLog.push('Service was not running');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 잠시 대기
 | 
				
			||||||
 | 
					                await new Promise(resolve => setTimeout(resolve, 2000));
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 시작
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    await this.startService(serviceName);
 | 
				
			||||||
 | 
					                    repairLog.push('Service started successfully');
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    repairLog.push(`Failed to start service: ${error.message}`);
 | 
				
			||||||
 | 
					                    throw error;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 4. 포트 문제 해결 (Ollama)
 | 
				
			||||||
 | 
					            if (serviceName === 'ollama' && diagnostics.checks.port?.status === 'fail') {
 | 
				
			||||||
 | 
					                repairLog.push('Port connectivity issue detected');
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 프로세스 강제 종료
 | 
				
			||||||
 | 
					                if (process.platform === 'darwin') {
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        const { exec } = require('child_process');
 | 
				
			||||||
 | 
					                        const { promisify } = require('util');
 | 
				
			||||||
 | 
					                        const execAsync = promisify(exec);
 | 
				
			||||||
 | 
					                        await execAsync('pkill -f ollama');
 | 
				
			||||||
 | 
					                        repairLog.push('Killed stale Ollama processes');
 | 
				
			||||||
 | 
					                    } catch (error) {
 | 
				
			||||||
 | 
					                        repairLog.push('No stale processes found');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else if (process.platform === 'win32') {
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        const { exec } = require('child_process');
 | 
				
			||||||
 | 
					                        const { promisify } = require('util');
 | 
				
			||||||
 | 
					                        const execAsync = promisify(exec);
 | 
				
			||||||
 | 
					                        await execAsync('taskkill /F /IM ollama.exe');
 | 
				
			||||||
 | 
					                        repairLog.push('Killed stale Ollama processes');
 | 
				
			||||||
 | 
					                    } catch (error) {
 | 
				
			||||||
 | 
					                        repairLog.push('No stale processes found');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else if (process.platform === 'linux') {
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        const { exec } = require('child_process');
 | 
				
			||||||
 | 
					                        const { promisify } = require('util');
 | 
				
			||||||
 | 
					                        const execAsync = promisify(exec);
 | 
				
			||||||
 | 
					                        await execAsync('pkill -f ollama');
 | 
				
			||||||
 | 
					                        repairLog.push('Killed stale Ollama processes');
 | 
				
			||||||
 | 
					                    } catch (error) {
 | 
				
			||||||
 | 
					                        repairLog.push('No stale processes found');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                await new Promise(resolve => setTimeout(resolve, 1000));
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 재시작
 | 
				
			||||||
 | 
					                await this.startService(serviceName);
 | 
				
			||||||
 | 
					                repairLog.push('Restarted service after port cleanup');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 5. Whisper 특화 복구
 | 
				
			||||||
 | 
					            if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					                // 세션 정리
 | 
				
			||||||
 | 
					                if (diagnostics.checks.running?.status === 'pass') {
 | 
				
			||||||
 | 
					                    repairLog.push('Cleaning up Whisper sessions...');
 | 
				
			||||||
 | 
					                    await service.cleanup();
 | 
				
			||||||
 | 
					                    repairLog.push('Sessions cleaned up');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // 초기화
 | 
				
			||||||
 | 
					                if (!service.installState.isInitialized) {
 | 
				
			||||||
 | 
					                    repairLog.push('Re-initializing Whisper...');
 | 
				
			||||||
 | 
					                    await service.initialize();
 | 
				
			||||||
 | 
					                    repairLog.push('Whisper re-initialized');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 6. 최종 상태 확인
 | 
				
			||||||
 | 
					            repairLog.push('Verifying repair...');
 | 
				
			||||||
 | 
					            const finalDiagnostics = await this.runDiagnostics(serviceName);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            const success = finalDiagnostics.summary.overallStatus === 'healthy';
 | 
				
			||||||
 | 
					            repairLog.push(success ? 'Repair successful!' : 'Repair failed - manual intervention may be required');
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 성공 시 상태 업데이트
 | 
				
			||||||
 | 
					            if (success) {
 | 
				
			||||||
 | 
					                await this.updateServiceState(serviceName);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                success,
 | 
				
			||||||
 | 
					                repairLog,
 | 
				
			||||||
 | 
					                diagnostics: finalDiagnostics
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            repairLog.push(`Repair error: ${error.message}`);
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                success: false,
 | 
				
			||||||
 | 
					                repairLog,
 | 
				
			||||||
 | 
					                error: error.message
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 상태 업데이트
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async updateServiceState(serviceName) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const status = await this.getServiceStatus(serviceName);
 | 
				
			||||||
 | 
					            this.state[serviceName] = status;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // 상태 변경 이벤트 발행
 | 
				
			||||||
 | 
					            this.emit('state-changed', serviceName, status);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error(`[LocalAIManager] Failed to update ${serviceName} state:`, error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 전체 상태 조회
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getAllServiceStates() {
 | 
				
			||||||
 | 
					        const states = {};
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for (const serviceName of Object.keys(this.services)) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                states[serviceName] = await this.getServiceStatus(serviceName);
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                states[serviceName] = {
 | 
				
			||||||
 | 
					                    success: false,
 | 
				
			||||||
 | 
					                    error: error.message
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return states;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 주기적 상태 동기화 시작
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    startPeriodicSync(interval = 30000) {
 | 
				
			||||||
 | 
					        if (this.syncInterval) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        this.syncInterval = setInterval(async () => {
 | 
				
			||||||
 | 
					            for (const serviceName of Object.keys(this.services)) {
 | 
				
			||||||
 | 
					                await this.updateServiceState(serviceName);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, interval);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 각 서비스의 주기적 동기화도 시작
 | 
				
			||||||
 | 
					        ollamaService.startPeriodicSync();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 주기적 상태 동기화 중지
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    stopPeriodicSync() {
 | 
				
			||||||
 | 
					        if (this.syncInterval) {
 | 
				
			||||||
 | 
					            clearInterval(this.syncInterval);
 | 
				
			||||||
 | 
					            this.syncInterval = null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 각 서비스의 주기적 동기화도 중지
 | 
				
			||||||
 | 
					        ollamaService.stopPeriodicSync();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 전체 종료
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async shutdown() {
 | 
				
			||||||
 | 
					        this.stopPeriodicSync();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const results = {};
 | 
				
			||||||
 | 
					        for (const [serviceName, service] of Object.entries(this.services)) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                if (serviceName === 'ollama') {
 | 
				
			||||||
 | 
					                    results[serviceName] = await service.shutdown(false);
 | 
				
			||||||
 | 
					                } else if (serviceName === 'whisper') {
 | 
				
			||||||
 | 
					                    await service.cleanup();
 | 
				
			||||||
 | 
					                    results[serviceName] = true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                results[serviceName] = false;
 | 
				
			||||||
 | 
					                console.error(`[LocalAIManager] Failed to shutdown ${serviceName}:`, error);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return results;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 에러 처리
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async handleError(serviceName, errorType, details = {}) {
 | 
				
			||||||
 | 
					        console.error(`[LocalAIManager] Error in ${serviceName}: ${errorType}`, details);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 서비스별 에러 처리
 | 
				
			||||||
 | 
					        switch(errorType) {
 | 
				
			||||||
 | 
					            case 'installation-failed':
 | 
				
			||||||
 | 
					                // 설치 실패 시 이벤트 발생
 | 
				
			||||||
 | 
					                this.emit('error-occurred', {
 | 
				
			||||||
 | 
					                    service: serviceName,
 | 
				
			||||||
 | 
					                    errorType,
 | 
				
			||||||
 | 
					                    error: details.error || 'Installation failed',
 | 
				
			||||||
 | 
					                    canRetry: true
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            case 'model-pull-failed':
 | 
				
			||||||
 | 
					            case 'model-download-failed':
 | 
				
			||||||
 | 
					                // 모델 다운로드 실패
 | 
				
			||||||
 | 
					                this.emit('error-occurred', {
 | 
				
			||||||
 | 
					                    service: serviceName,
 | 
				
			||||||
 | 
					                    errorType,
 | 
				
			||||||
 | 
					                    model: details.model,
 | 
				
			||||||
 | 
					                    error: details.error || 'Model download failed',
 | 
				
			||||||
 | 
					                    canRetry: true
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            case 'service-not-responding':
 | 
				
			||||||
 | 
					                // 서비스 반응 없음
 | 
				
			||||||
 | 
					                console.log(`[LocalAIManager] Attempting to repair ${serviceName}...`);
 | 
				
			||||||
 | 
					                const repairResult = await this.repairService(serviceName);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                this.emit('error-occurred', {
 | 
				
			||||||
 | 
					                    service: serviceName,
 | 
				
			||||||
 | 
					                    errorType,
 | 
				
			||||||
 | 
					                    error: details.error || 'Service not responding',
 | 
				
			||||||
 | 
					                    repairAttempted: true,
 | 
				
			||||||
 | 
					                    repairSuccessful: repairResult.success
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                // 기타 에러
 | 
				
			||||||
 | 
					                this.emit('error-occurred', {
 | 
				
			||||||
 | 
					                    service: serviceName,
 | 
				
			||||||
 | 
					                    errorType,
 | 
				
			||||||
 | 
					                    error: details.error || `Unknown error: ${errorType}`,
 | 
				
			||||||
 | 
					                    canRetry: false
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 싱글톤
 | 
				
			||||||
 | 
					const localAIManager = new LocalAIManager();
 | 
				
			||||||
 | 
					module.exports = localAIManager;
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user