- 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
143 lines
4.8 KiB
TypeScript
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)
|
|
}
|
|
})
|
|
}
|