Knowledge documents travel as source-text chunks; each surface embeds them with its own model, the server index is requested through embed-chunks, and documents from the phone are stored without a file and indexed from their chunks. Chunk text is now kept when local embedding fails, so reindexing no longer needs the original file. Recordings upload to the mobile storage contract (audio bucket under the user's folder plus an audio_files row, 50 MiB cap, a Settings > Cloud toggle) and are removed with their record. The history card gains a play button that uses the local file or, for phone recordings, a signed URL. Language (ko/en), system/light/dark theme, auto-polish and the active user command follow the phone's user_settings with its revision rule; changes that arrive from the phone reach the open window.
115 lines
4.6 KiB
TypeScript
115 lines
4.6 KiB
TypeScript
// src/main/ipc/history-handlers.ts
|
|
|
|
import { ipcMain } from 'electron'
|
|
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
|
import { ipcSuccess, ipcError, ErrorCode } from '@d3ro/core/errors'
|
|
import fs from 'fs'
|
|
import { getHistoryService } from '../services/HistoryService'
|
|
import { getCloudSyncService } from '../services/CloudSyncService'
|
|
import { audioMimeType } from '../services/sync/audio-sync'
|
|
import type {
|
|
HistoryQueryParams,
|
|
HistoryGetByIdParams,
|
|
HistoryDeleteParams,
|
|
HistorySearchParams,
|
|
HistorySetFavoriteParams,
|
|
HistoryAudioSource
|
|
} from '@d3ro/core/types'
|
|
|
|
/** 렌더러로 넘기는 로컬 녹음 상한. 넘으면 재생하지 않는다(IPC 복사 비용). */
|
|
const MAX_LOCAL_AUDIO_BYTES = 100 * 1024 * 1024
|
|
|
|
export function registerHistoryHandlers(): void {
|
|
ipcMain.handle(IPC_CHANNELS.HISTORY.GET_ALL, async (_event, params: HistoryQueryParams) => {
|
|
try {
|
|
return ipcSuccess(getHistoryService().list(params))
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBQueryFailed, `Failed to get history: ${message}`)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC_CHANNELS.HISTORY.GET_BY_ID, async (_event, params: HistoryGetByIdParams) => {
|
|
try {
|
|
return ipcSuccess(getHistoryService().getById(params.id))
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBQueryFailed, `Failed to get history entry: ${message}`)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC_CHANNELS.HISTORY.DELETE, async (_event, params: HistoryDeleteParams) => {
|
|
try {
|
|
const removed = getHistoryService().delete(params.id)
|
|
if (!removed) {
|
|
return ipcError(ErrorCode.HistoryNotFound, `History entry not found: ${params.id}`)
|
|
}
|
|
return ipcSuccess(undefined)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBWriteFailed, `Failed to delete history entry: ${message}`)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC_CHANNELS.HISTORY.DELETE_ALL, async () => {
|
|
try {
|
|
getHistoryService().deleteAll()
|
|
return ipcSuccess(undefined)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBWriteFailed, `Failed to delete all history: ${message}`)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC_CHANNELS.HISTORY.SET_FAVORITE, async (_event, params: HistorySetFavoriteParams) => {
|
|
try {
|
|
const entry = getHistoryService().setFavorite(params.id, params.isFavorite === true)
|
|
if (!entry) {
|
|
return ipcError(ErrorCode.HistoryNotFound, `History entry not found: ${params.id}`)
|
|
}
|
|
return ipcSuccess(entry)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBWriteFailed, `Failed to update favorite: ${message}`)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC_CHANNELS.HISTORY.GET_AUDIO, async (_event, params: HistoryGetByIdParams) => {
|
|
try {
|
|
const entry = getHistoryService().getById(params.id)
|
|
if (!entry) return ipcError(ErrorCode.HistoryNotFound, `History entry not found: ${params.id}`)
|
|
const localPath = entry.audioLocalPath
|
|
if (localPath && fs.existsSync(localPath) && fs.statSync(localPath).size <= MAX_LOCAL_AUDIO_BYTES) {
|
|
const source: HistoryAudioSource = {
|
|
kind: 'local',
|
|
bytes: new Uint8Array(fs.readFileSync(localPath)),
|
|
mimeType: audioMimeType(localPath) ?? 'audio/wav',
|
|
}
|
|
return ipcSuccess(source)
|
|
}
|
|
const url = await getCloudSyncService().createHistoryAudioUrl(params.id)
|
|
return ipcSuccess(url ? ({ kind: 'remote', url } satisfies HistoryAudioSource) : null)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBQueryFailed, `Failed to load audio: ${message}`)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC_CHANNELS.HISTORY.SEARCH, async (_event, params: HistorySearchParams) => {
|
|
try {
|
|
return ipcSuccess(getHistoryService().search(params))
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBQueryFailed, `Failed to search history: ${message}`)
|
|
}
|
|
})
|
|
|
|
ipcMain.handle(IPC_CHANNELS.STATS.GET_SUMMARY, async () => {
|
|
try {
|
|
return ipcSuccess(getHistoryService().getStats())
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
return ipcError(ErrorCode.DBQueryFailed, `Failed to get stats: ${message}`)
|
|
}
|
|
})
|
|
}
|