diff --git a/client/src/features/settings/GeneralSettingsPage.tsx b/client/src/features/settings/GeneralSettingsPage.tsx index 79e7273..a5b4506 100644 --- a/client/src/features/settings/GeneralSettingsPage.tsx +++ b/client/src/features/settings/GeneralSettingsPage.tsx @@ -29,6 +29,7 @@ export default function GeneralSettingsPage() { const [protectClipboard, setProtectClipboard] = useState(true) const [pttKey, setPttKey] = useState('AltLeft') const [handsFreeKey, setHandsFreeKey] = useState('Alt+L') + const [historyEnabled, setHistoryEnabled] = useState(true) const [audioRetentionEnabled, setAudioRetentionEnabled] = useState(true) const [audioRetentionDays, setAudioRetentionDays] = useState(30) const [logRetentionDays, setLogRetentionDays] = useState(30) @@ -45,6 +46,7 @@ export default function GeneralSettingsPage() { getSetting('protectClipboard', true).then((value) => setProtectClipboard(Boolean(value))) getSetting('shortcutPTT', 'AltRight').then((value) => setPttKey(value as string)) getSetting('shortcutHandsFree', 'Alt+L').then((value) => setHandsFreeKey(value as string)) + getSetting('historyEnabled', true).then((value) => setHistoryEnabled(Boolean(value))) getSetting('audioRetentionEnabled', true).then((value) => setAudioRetentionEnabled(Boolean(value))) getSetting('readySoundEnabled', true).then((value) => setReadySoundEnabled(Boolean(value))) getSetting('audioRetentionDays', -1).then((value) => { @@ -55,7 +57,7 @@ export default function GeneralSettingsPage() { const v = Number(value) if (v === 7 || v === 15 || v === 30 || v === 90) setLogRetentionDays(v) }) - listMicrophones().then(setMics).catch(() => {}) + listMicrophones().then(setMics).catch(() => { }) }, []) const toggleAutoLaunch = async () => { const next = !autoLaunch; setAutoLaunch(next); await bridge.setAutoLaunch(next) } @@ -63,6 +65,7 @@ export default function GeneralSettingsPage() { const handleMicChange = async (deviceId: string) => { setSelectedMic(deviceId); await setSetting('selectedMic', deviceId); await refreshRecorderSettings() } const toggleMuteSystemAudio = async () => { const next = !muteSystemAudio; setMuteSystemAudio(next); await setSetting('muteSystemAudioWhileRecording', next); await refreshRecorderSettings() } const toggleProtectClipboard = async () => { const next = !protectClipboard; setProtectClipboard(next); await setSetting('protectClipboard', next); await refreshRecorderSettings() } + const toggleHistoryEnabled = async () => { const next = !historyEnabled; setHistoryEnabled(next); await setSetting('historyEnabled', next) } const toggleAudioRetention = async () => { const next = !audioRetentionEnabled; setAudioRetentionEnabled(next); await setSetting('audioRetentionEnabled', next) } const toggleReadySound = async () => { const next = !readySoundEnabled; setReadySoundEnabled(next); await setSetting('readySoundEnabled', next); await refreshRecorderSettings() } const handleAudioRetentionDaysChange = async (value: number) => { setAudioRetentionDays(value); await setSetting('audioRetentionDays', value) } @@ -173,6 +176,18 @@ export default function GeneralSettingsPage() { + + +
+
+

历史记录

+

保存每次转写的文本与录音到本地历史,可随时回看、复制和重新识别。关闭后不再保存新的记录(适合与他人共用的电脑);已有记录不会被删除,可在历史页手动清除。

+
+ void toggleHistoryEnabled()} /> +
+
+
+
diff --git a/client/src/services/defaults.ts b/client/src/services/defaults.ts index 1d0afa5..9adfaaf 100644 --- a/client/src/services/defaults.ts +++ b/client/src/services/defaults.ts @@ -69,6 +69,7 @@ export const DEFAULTS: Record = { // ── 应用设置 ── autoCheckUpdate: true, // 自动检查更新。可选: true | false + historyEnabled: true, // 保存历史记录(文本+录音)。关闭后不再保存新记录,适合共享电脑。可选: true | false audioRetentionEnabled: true, // 保留录音文件。可选: true | false audioRetentionDays: -1, // 录音保留天数。可选: 7 | 30 | 90 | -1(永久) logRetentionDays: 30, // 日志保留天数。可选: 7 | 15 | 30 | 90 diff --git a/client/src/services/recorder/RecorderOrchestrator.ts b/client/src/services/recorder/RecorderOrchestrator.ts index 5b557be..617ff6e 100644 --- a/client/src/services/recorder/RecorderOrchestrator.ts +++ b/client/src/services/recorder/RecorderOrchestrator.ts @@ -603,6 +603,7 @@ export class RecorderOrchestrator { const wallSec = this.wallTimeAtStopSec > 0 ? this.wallTimeAtStopSec : audioDur // Save audio even for empty results const saveAndRecord = async () => { + if (!(await getSetting('historyEnabled', true))) return let audioFilePath: string | undefined const recordId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) const saveAudioEnabled = await getSetting('audioRetentionEnabled', true) @@ -703,6 +704,7 @@ export class RecorderOrchestrator { const wallSec = this.wallTimeAtStopSec > 0 ? this.wallTimeAtStopSec : audioDur if (audioDur >= 0.5 && this.recordedChunks.length > 0) { const saveErrorHistory = async () => { + if (!(await getSetting('historyEnabled', true))) return let audioFilePath: string | undefined const recordId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) const saveAudioEnabled = await getSetting('audioRetentionEnabled', true) @@ -1353,6 +1355,7 @@ export class RecorderOrchestrator { this.timedOutProcessingContext = null void (async () => { try { + if (!(await getSetting('historyEnabled', true))) return let audioFilePath: string | undefined const recordId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) const saveAudioEnabled = await getSetting('audioRetentionEnabled', true) @@ -1614,38 +1617,41 @@ export class RecorderOrchestrator { }) try { - // Save audio file if we have recorded chunks - let audioFilePath: string | undefined - const recordId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) - const saveAudioEnabled = await getSetting('audioRetentionEnabled', true) - if (saveAudioEnabled && this.recordedChunks.length > 0) { - try { - const savedPath = await saveRecordingAudio(recordId, this.recordedChunks) - if (savedPath) audioFilePath = savedPath - } catch (err) { - addRuntimeEvent('warn', 'recorder', '保存音频文件失败', { error: String(err) }) + // 关闭历史记录时不保存任何记录与音频(共享电脑隐私),但仍完成文本插入 + if (await getSetting('historyEnabled', true)) { + // Save audio file if we have recorded chunks + let audioFilePath: string | undefined + const recordId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) + const saveAudioEnabled = await getSetting('audioRetentionEnabled', true) + if (saveAudioEnabled && this.recordedChunks.length > 0) { + try { + const savedPath = await saveRecordingAudio(recordId, this.recordedChunks) + if (savedPath) audioFilePath = savedPath + } catch (err) { + addRuntimeEvent('warn', 'recorder', '保存音频文件失败', { error: String(err) }) + } } + + const providerMeta = await this.buildProviderMetadata(result) + + await addHistory({ + id: recordId, + timestamp: Date.now(), + asrText: result.asrText, + llmText: textToPaste, + asrMs: result.asrMs, + llmMs: result.llmMs, + durationSec: wallSec, + audioDurationSec: audioDur > 0 ? audioDur : undefined, + asrDurationSec: result.durationSec > 0 ? result.durationSec : undefined, + charCount: hasText ? textToPaste.length : 0, + isEmpty: !hasText, + audioFilePath, + ...this.buildHistoryMetadata(promptResolution), + ...providerMeta, + }) + void bridge.emit('history-updated') } - - const providerMeta = await this.buildProviderMetadata(result) - - await addHistory({ - id: recordId, - timestamp: Date.now(), - asrText: result.asrText, - llmText: textToPaste, - asrMs: result.asrMs, - llmMs: result.llmMs, - durationSec: wallSec, - audioDurationSec: audioDur > 0 ? audioDur : undefined, - asrDurationSec: result.durationSec > 0 ? result.durationSec : undefined, - charCount: hasText ? textToPaste.length : 0, - isEmpty: !hasText, - audioFilePath, - ...this.buildHistoryMetadata(promptResolution), - ...providerMeta, - }) - void bridge.emit('history-updated') } catch (error) { addRuntimeEvent('warn', 'recorder', '写入历史记录失败', { error: String(error) }) }