// src/main/ipc/llm-handlers.ts import { ipcMain } from 'electron' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { ipcSuccess, ipcError, ErrorCode } from '@d3ro/core/errors' import { getLocalLLMService } from '../services/LocalLLMService' import { getPremiumLLMService } from '../services/PremiumLLMService' import { getVoiceModeService } from '../services/VoiceModeService' import { configGet, configSet } from '../services/ConfigService' import { getMainWindow } from '../windows/WindowManager' import type { SetLLMModelParams, SetServerUrlParams, LLMProcessParams } from '@d3ro/core/types' function safeSendToRenderer(channel: string, data: unknown): void { const win = getMainWindow() if (win && !win.isDestroyed()) { win.webContents.send(channel, data) } } export function registerLLMHandlers(): void { // LLM 가용성 변경 시 렌더러에 상태 전파 const llm = getLocalLLMService() llm.on('availability-changed', () => { safeSendToRenderer(IPC_CHANNELS.LLM.STATUS_CHANGED, { status: llm.getStatus() }) }) // Pull 진행률 → 렌더러 llm.on('pull-progress', (payload: unknown) => { safeSendToRenderer(IPC_CHANNELS.LLM.PULL_PROGRESS, payload) }) ipcMain.handle(IPC_CHANNELS.LLM.PULL_MODEL, async (_event, params: { modelId: string }) => { try { await getLocalLLMService().pullModel(params.modelId) return ipcSuccess(undefined) } catch (err) { const msg = err instanceof Error ? err.message : String(err) return ipcError(ErrorCode.LLMServerUnreachable, `Pull 실패: ${msg}`) } }) // Phase 3.2: VoiceModeService의 premium-llm-fallback 이벤트를 렌더러로 전달 getVoiceModeService().on('premium-llm-fallback', (payload: { reason: string }) => { safeSendToRenderer(IPC_CHANNELS.LLM.PREMIUM_FALLBACK, payload) }) // Phase 3.2: PremiumLLMService 이벤트 → 렌더러 const premium = getPremiumLLMService() premium.on('quota-warning', (payload) => { safeSendToRenderer(IPC_CHANNELS.LLM.PREMIUM_QUOTA_WARNING, payload) }) premium.on('upgrade-required', (payload) => { safeSendToRenderer(IPC_CHANNELS.LLM.PREMIUM_UPGRADE_REQUIRED, payload) }) ipcMain.handle(IPC_CHANNELS.LLM.GET_STATUS, async () => { return ipcSuccess(getLocalLLMService().getStatus()) }) ipcMain.handle(IPC_CHANNELS.LLM.GET_MODELS, async () => { try { const models = await getLocalLLMService().getModels() return ipcSuccess(models) } catch (error) { const message = error instanceof Error ? error.message : String(error) return ipcError(ErrorCode.LLMServerUnreachable, `Failed to get models: ${message}`) } }) ipcMain.handle(IPC_CHANNELS.LLM.GET_ACTIVE_MODEL, async () => { return ipcSuccess(configGet('llmModelId')) }) ipcMain.handle(IPC_CHANNELS.LLM.SET_MODEL, async (_event, params: SetLLMModelParams) => { configSet('llmModelId', params.modelId) return ipcSuccess(undefined) }) ipcMain.handle(IPC_CHANNELS.LLM.PROCESS, async (_event, params: LLMProcessParams) => { try { const llm = getLocalLLMService() const start = performance.now() const processedText = await llm.processText( params.text, params.action, params.targetLanguage, params.customPrompt ) return ipcSuccess({ originalText: params.text, processedText, action: params.action, processingTimeMs: Math.round(performance.now() - start), tokenCount: 0 }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return ipcError(ErrorCode.LLMProcessingFailed, `LLM processing failed: ${message}`) } }) ipcMain.handle(IPC_CHANNELS.LLM.CANCEL_PROCESS, async () => { getLocalLLMService().cancelGeneration() return ipcSuccess(undefined) }) ipcMain.handle(IPC_CHANNELS.LLM.GET_SERVER_URL, async () => { return ipcSuccess(configGet('ollamaServerUrl')) }) ipcMain.handle(IPC_CHANNELS.LLM.SET_SERVER_URL, async (_event, params: SetServerUrlParams) => { configSet('ollamaServerUrl', params.url) return ipcSuccess(undefined) }) // Phase 3.2: Premium LLM 상태/쿼터 조회 ipcMain.handle(IPC_CHANNELS.LLM.PREMIUM_GET_STATUS, async () => { const premium = getPremiumLLMService() return ipcSuccess({ available: premium.isAvailable(), backend: configGet('llmBackend') }) }) ipcMain.handle(IPC_CHANNELS.LLM.PREMIUM_GET_QUOTA, async () => { const premium = getPremiumLLMService() const snapshot = premium.getLastQuota() return ipcSuccess(snapshot) }) }