d3ro-voice/apps/desktop/src/main/ipc/voice-conversation-handlers.ts
Yun Chan aef44289a5 refactor(main): IPC 채널 SSOT 일원화 + AppConfig 타입 강화 (WS2)
- ipc-channels.ts SSOT에 9개 그룹/키 추가: INSTRUCTION, SYSTEM_AUDIO,
  POPUP_RESULT/HISTORY/COMMAND/CAPTION, VOICE_PARTIAL, CLIPBOARD, APP,
  VOICE_CONVERSATION.FINISH_LISTENING
- WindowManager/handlers/bootstrap 하드코딩 채널 -> IPC_CHANNELS 교체.
  불일치 4건(result:* vs window:tip*)은 preload 재검증 후 SSOT로 통일.
  notifyRenderer channel: string -> IPCChannel 타입 좁힘.
- AppConfig에 7개 누락 키 추가(customInstructions, llmChains,
  voiceCommandRules, voiceCommandsEnabled, activeInstructionId,
  activeChainId, captionAudioSource) -> as never 16건 제거.
- ChainService/CustomInstructionService/WindowManager dynamic require -> static import.
- services/index.ts 데드 레지스트리 제거 (import 0건).
SKIP: ipcSuccess/ipcError 헬퍼 통일, catch 패턴(별도)
정책: docs/REFACTOR_POLICY.md DP2, DP9
2026-07-22 01:54:18 +09:00

143 lines
4.8 KiB
TypeScript

// src/main/ipc/voice-conversation-handlers.ts
// Phase 13.1: 음성 대화 모드 IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@d3ro/core/errors'
import { getVoiceConversationService } from '../services/VoiceConversationService'
import { getCloudSyncService } from '../services/CloudSyncService'
import { getLogger } from '../services/LoggerService'
import type {
ConversationSendParams,
RealtimeTokenParams,
RealtimeTokenResult,
} from '@d3ro/core/types'
const logger = getLogger('voice-conversation-handlers')
export function registerVoiceConversationHandlers(): void {
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.START_SESSION, async () => {
try {
await getVoiceConversationService().startSession()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Conversation start failed:', msg)
return ipcError(ErrorCode.ConversationSessionAlreadyActive, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.STOP_SESSION, async () => {
try {
getVoiceConversationService().stopSession()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.ConversationNoActiveSession, msg)
}
})
ipcMain.handle(
IPC_CHANNELS.VOICE_CONVERSATION.SEND_MESSAGE,
async (_event, params: ConversationSendParams) => {
try {
await getVoiceConversationService().sendTextMessage(params.text)
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Conversation send failed:', msg)
return ipcError(ErrorCode.ConversationLLMFailed, msg)
}
},
)
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.GET_STATE, async () => {
try {
return ipcSuccess(getVoiceConversationService().getSessionInfo())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.GET_HISTORY, async () => {
try {
return ipcSuccess(getVoiceConversationService().getHistory())
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.CLEAR_HISTORY, async () => {
try {
getVoiceConversationService().clearHistory()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.CANCEL_RESPONSE, async () => {
try {
getVoiceConversationService().cancelResponse()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.UnknownError, msg)
}
})
// OpenAI Realtime ephemeral token — 렌더러가 WebRTC 직결에 사용
ipcMain.handle(
IPC_CHANNELS.VOICE_CONVERSATION.GET_REALTIME_TOKEN,
async (_event, params: RealtimeTokenParams) => {
try {
const { data, error } = await getCloudSyncService().invokeFunction(
'realtime-token',
{ ...params },
)
if (error) {
return ipcError(ErrorCode.ConversationRealtimeTokenFailed, error.message)
}
const raw = data as {
value?: string
expires_at?: number
model?: string
tier?: string
error?: string
message?: string
}
if (!raw.value) {
return ipcError(
ErrorCode.ConversationRealtimeTokenFailed,
raw.error ?? raw.message ?? 'No token in response',
)
}
const result: RealtimeTokenResult = {
value: raw.value,
expiresAt: raw.expires_at ?? 0,
model: raw.model ?? 'unknown',
tier: (raw.tier as RealtimeTokenResult['tier']) ?? 'free',
}
return ipcSuccess(result)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
logger.error('Realtime token request failed:', msg)
return ipcError(ErrorCode.ConversationRealtimeTokenFailed, msg)
}
},
)
// finishListening — 렌더러에서 녹음 종료 버튼 클릭 시
ipcMain.handle(IPC_CHANNELS.VOICE_CONVERSATION.FINISH_LISTENING, async () => {
try {
await getVoiceConversationService().finishListening()
return ipcSuccess(undefined)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return ipcError(ErrorCode.ConversationLLMFailed, msg)
}
})
}