fix(desktop): surface configuration and provider failures instead of hiding them
Several desktop paths quietly substituted defaults or partial results: a config write could fall back to a throwaway in-memory store, speech provider errors were absorbed into empty transcriptions, and meeting exports built file names from raw titles. Writes now fail explicitly when the store is unavailable, provider and model failures reach the UI as errors, and export names pass through one sanitizer. Settings, license, ad, and support surfaces use the shared theme tokens, unused hotkey helpers are gone, and the package gains strict node/renderer typecheck configs plus red-team e2e scenarios for these flows.
This commit is contained in:
parent
c8d802d78f
commit
6ba25f53b7
43 changed files with 1721 additions and 204 deletions
|
|
@ -59,7 +59,6 @@ class MeetingModeService extends EventEmitter {
|
|||
|
||||
/** Phase 15.5-2: 녹음 중 오디오 WAV 파일 보존 */
|
||||
private _audioBuffersForFile: Buffer[] = []
|
||||
private _audioFilePath: string | null = null
|
||||
|
||||
// Phase 15: Meeting AI Chat
|
||||
private _chatHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []
|
||||
|
|
@ -369,7 +368,6 @@ class MeetingModeService extends EventEmitter {
|
|||
if (!fs.existsSync(audioDir)) fs.mkdirSync(audioDir, { recursive: true })
|
||||
const wavPath = path.join(audioDir, `${sessionId}.wav`)
|
||||
this._saveWav(wavPath, Buffer.concat(this._audioBuffersForFile))
|
||||
this._audioFilePath = wavPath
|
||||
logger.info(`회의 오디오 저장: ${wavPath}`)
|
||||
} catch (err) {
|
||||
logger.warn(`오디오 저장 실패: ${err instanceof Error ? err.message : String(err)}`)
|
||||
|
|
@ -560,7 +558,6 @@ class MeetingModeService extends EventEmitter {
|
|||
this._memos = []
|
||||
this._meetingModeActive = false
|
||||
this._audioBuffersForFile = []
|
||||
this._audioFilePath = null
|
||||
}
|
||||
|
||||
/** PCM16 버퍼를 WAV 파일로 저장 (16kHz mono 16-bit) */
|
||||
|
|
@ -745,7 +742,7 @@ class MeetingModeService extends EventEmitter {
|
|||
title: docTitle,
|
||||
content: result.text,
|
||||
promptUsed: systemPrompt,
|
||||
llmModel: result.model,
|
||||
llmModel: result.model ?? null,
|
||||
llmLatencyMs,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
|
@ -797,7 +794,7 @@ class MeetingModeService extends EventEmitter {
|
|||
|
||||
// ── Phase 14.5: 문서 내보내기 ──
|
||||
|
||||
async exportDocument(documentId: string, format: MeetingExportFormat): Promise<string> {
|
||||
async exportDocument(documentId: string, format: MeetingExportFormat, targetPath?: string): Promise<string> {
|
||||
const db = getDatabase()
|
||||
const row = db.select().from(meetingDocuments).where(eq(meetingDocuments.id, documentId)).get()
|
||||
if (!row) {
|
||||
|
|
@ -829,25 +826,27 @@ class MeetingModeService extends EventEmitter {
|
|||
break
|
||||
}
|
||||
|
||||
if (isShowingMeetingSaveDialog) {
|
||||
return ''
|
||||
}
|
||||
isShowingMeetingSaveDialog = true
|
||||
let filePath: string | undefined
|
||||
try {
|
||||
const { getMainWindow } = await import('../windows/WindowManager')
|
||||
const mainWindow = getMainWindow()
|
||||
const dialogOptions = {
|
||||
title: '문서 내보내기',
|
||||
defaultPath: defaultName,
|
||||
filters,
|
||||
let filePath: string | undefined = targetPath
|
||||
if (!filePath) {
|
||||
if (isShowingMeetingSaveDialog) {
|
||||
return ''
|
||||
}
|
||||
isShowingMeetingSaveDialog = true
|
||||
try {
|
||||
const { getMainWindow } = await import('../windows/WindowManager')
|
||||
const mainWindow = getMainWindow()
|
||||
const dialogOptions = {
|
||||
title: '문서 내보내기',
|
||||
defaultPath: defaultName,
|
||||
filters,
|
||||
}
|
||||
const res = mainWindow
|
||||
? await dialog.showSaveDialog(mainWindow, dialogOptions)
|
||||
: await dialog.showSaveDialog(dialogOptions)
|
||||
filePath = res.filePath
|
||||
} finally {
|
||||
isShowingMeetingSaveDialog = false
|
||||
}
|
||||
const res = mainWindow
|
||||
? await dialog.showSaveDialog(mainWindow, dialogOptions)
|
||||
: await dialog.showSaveDialog(dialogOptions)
|
||||
filePath = res.filePath
|
||||
} finally {
|
||||
isShowingMeetingSaveDialog = false
|
||||
}
|
||||
if (!filePath) return ''
|
||||
|
||||
|
|
@ -900,7 +899,7 @@ class MeetingModeService extends EventEmitter {
|
|||
return filePath
|
||||
}
|
||||
|
||||
async exportTranscript(sessionId: string): Promise<string> {
|
||||
async exportTranscript(sessionId: string, targetPath?: string): Promise<string> {
|
||||
const db = getDatabase()
|
||||
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
|
||||
if (!row) {
|
||||
|
|
@ -911,11 +910,15 @@ class MeetingModeService extends EventEmitter {
|
|||
const dateStr = formatDateFile(row.startedAt)
|
||||
const safeTitle = (row.title ?? '무제_회의').replace(/[/\\?%*:|"<>]/g, '-').slice(0, 50)
|
||||
|
||||
const { filePath } = await dialog.showSaveDialog({
|
||||
title: '전사 내보내기',
|
||||
defaultPath: `전사_${safeTitle}_${dateStr}.txt`,
|
||||
filters: [{ name: 'Text', extensions: ['txt'] }],
|
||||
})
|
||||
let filePath: string | undefined = targetPath
|
||||
if (!filePath) {
|
||||
const res = await dialog.showSaveDialog({
|
||||
title: '전사 내보내기',
|
||||
defaultPath: `전사_${safeTitle}_${dateStr}.txt`,
|
||||
filters: [{ name: 'Text', extensions: ['txt'] }],
|
||||
})
|
||||
filePath = res.filePath
|
||||
}
|
||||
if (!filePath) return ''
|
||||
|
||||
fs.writeFileSync(filePath, transcript, 'utf-8')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue