From 29809d9cd3a40a9c2c26ae6a91134c228526a58c Mon Sep 17 00:00:00 2001 From: Melorise <0d00@0721.hk> Date: Fri, 4 Sep 2026 17:32:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8D=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E5=81=9C=E6=AD=A2=E9=87=8D=E6=96=B0=E5=BC=80=E5=90=AF=E7=BD=91?= =?UTF-8?q?=E9=A1=B5=E5=BD=95=E9=9F=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 停止请求早于网页录音状态落地时,等待实际进入录音后再点击一次停止;网页仍在录音或尚未点击停止时不再完成外部会话。 AI-assisted-by: Codex AI-model: gpt-5.6-sol AI-reasoning-effort: medium Co-authored-by: Codex --- src/preload/backend.ts | 23 ++++++++++++++++------- src/preload/speech-session-state.ts | 4 ++++ src/preload/stop-completion.ts | 4 ++-- test/speech-session-state.test.ts | 11 ++++++++++- test/stop-completion.test.ts | 14 +++++++++++--- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/preload/backend.ts b/src/preload/backend.ts index b2524e1..0084a7d 100644 --- a/src/preload/backend.ts +++ b/src/preload/backend.ts @@ -1,7 +1,7 @@ import { ipcRenderer } from 'electron'; import type { BackendControlCommand, BackendPageStatus, BackendTranscriptPayload } from '../backends/contracts'; import { getBackendWebAdapter } from '../backends/web-registry'; -import { shouldClickMicrophoneOnCancel } from './speech-session-state'; +import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from './speech-session-state'; import { shouldFinishAfterStop } from './stop-completion'; const backendId = process.argv @@ -180,9 +180,7 @@ function stopSpeech(): void { if (!active || stopping || !adapter) return; stopping = true; const stoppedAt = Date.now(); - const wasRecordingAtStop = adapter.isRecording(document); - adapter.findMicrophone(document)?.click(); - diagnose('stop-clicked', { microphone: controlSnapshot(adapter.findMicrophone(document)) }); + let stopClicked = false; const finish = (): void => { emitCurrentEditorText(); resetSession(); @@ -192,11 +190,22 @@ function stopSpeech(): void { const checkCompletion = (): void => { if (!active || !stopping || !adapter) return; const now = Date.now(); + const isRecording = adapter.isRecording(document); + if (shouldClickMicrophoneOnStop(isRecording, stopClicked)) { + const microphone = adapter.findMicrophone(document); + if (microphone) { + microphone.click(); + stopClicked = true; + diagnose('stop-clicked', { microphone: controlSnapshot(adapter.findMicrophone(document)) }); + } + stopTimer = setTimeout(checkCompletion, 75); + return; + } if (shouldFinishAfterStop({ elapsedMs: now - stoppedAt, quietMs: now - lastTranscriptAt, - wasRecordingAtStop, - isRecording: adapter.isRecording(document), + stopClicked, + isRecording, ...adapter.stopCompletion })) { diagnose('stop-complete', { elapsedMs: now - stoppedAt }); @@ -206,7 +215,7 @@ function stopSpeech(): void { stopTimer = setTimeout(checkCompletion, 75); }; clearTimeout(stopTimer); - stopTimer = setTimeout(checkCompletion, 75); + checkCompletion(); } function cancelSpeech(): void { diff --git a/src/preload/speech-session-state.ts b/src/preload/speech-session-state.ts index a819dc3..ddd2f47 100644 --- a/src/preload/speech-session-state.ts +++ b/src/preload/speech-session-state.ts @@ -1,3 +1,7 @@ export function shouldClickMicrophoneOnCancel(active: boolean, stopping: boolean): boolean { return active && !stopping; } + +export function shouldClickMicrophoneOnStop(isRecording: boolean, stopClicked: boolean): boolean { + return isRecording && !stopClicked; +} diff --git a/src/preload/stop-completion.ts b/src/preload/stop-completion.ts index fd4abb5..2918725 100644 --- a/src/preload/stop-completion.ts +++ b/src/preload/stop-completion.ts @@ -1,7 +1,7 @@ export interface StopCompletionInput { readonly elapsedMs: number; readonly quietMs: number; - readonly wasRecordingAtStop: boolean; + readonly stopClicked: boolean; readonly isRecording: boolean; readonly minimumWaitMs: number; readonly quietWindowMs: number; @@ -9,7 +9,7 @@ export interface StopCompletionInput { } export function shouldFinishAfterStop(input: StopCompletionInput): boolean { + if (!input.stopClicked || input.isRecording) return false; if (input.elapsedMs >= input.timeoutMs) return true; - if (input.wasRecordingAtStop && input.isRecording) return false; return input.elapsedMs >= input.minimumWaitMs && input.quietMs >= input.quietWindowMs; } diff --git a/test/speech-session-state.test.ts b/test/speech-session-state.test.ts index fd2bd74..bbdea41 100644 --- a/test/speech-session-state.test.ts +++ b/test/speech-session-state.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { shouldClickMicrophoneOnCancel } from '../src/preload/speech-session-state'; +import { shouldClickMicrophoneOnCancel, shouldClickMicrophoneOnStop } from '../src/preload/speech-session-state'; test('停止命令已经点击过麦克风后,取消不得再次点击并重新开启录音', () => { assert.equal(shouldClickMicrophoneOnCancel(true, true), false); @@ -13,3 +13,12 @@ test('录音期间直接取消时仍应点击麦克风停止网页录音', () => test('本地会话未激活时取消不得点击麦克风', () => { assert.equal(shouldClickMicrophoneOnCancel(false, false), false); }); + +test('停止到达但网页尚未开始录音时不得再次点击开始按钮', () => { + assert.equal(shouldClickMicrophoneOnStop(false, false), false); +}); + +test('网页异步进入录音后只点击一次停止按钮', () => { + assert.equal(shouldClickMicrophoneOnStop(true, false), true); + assert.equal(shouldClickMicrophoneOnStop(true, true), false); +}); diff --git a/test/stop-completion.test.ts b/test/stop-completion.test.ts index 91ec8ea..611ddf4 100644 --- a/test/stop-completion.test.ts +++ b/test/stop-completion.test.ts @@ -5,7 +5,7 @@ import { shouldFinishAfterStop } from '../src/preload/stop-completion'; const base = { elapsedMs: 700, quietMs: 500, - wasRecordingAtStop: true, + stopClicked: true, isRecording: false, minimumWaitMs: 250, quietWindowMs: 450, @@ -16,6 +16,10 @@ test('网页仍在录音时不得提前提交', () => { assert.equal(shouldFinishAfterStop({ ...base, isRecording: true }), false); }); +test('尚未点击停止时不得提交', () => { + assert.equal(shouldFinishAfterStop({ ...base, stopClicked: false }), false); +}); + test('网页结束且最终文本静默窗口满足时立即提交', () => { assert.equal(shouldFinishAfterStop(base), true); }); @@ -24,6 +28,10 @@ test('网页结束后仍有文本修正时继续等待', () => { assert.equal(shouldFinishAfterStop({ ...base, quietMs: 120 }), false); }); -test('网页状态未回切时由超时兜底结束', () => { - assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), true); +test('网页仍在录音时超时也不得提交', () => { + assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, isRecording: true }), false); +}); + +test('网页已停止时超时可以结束静默等待', () => { + assert.equal(shouldFinishAfterStop({ ...base, elapsedMs: 4_500, quietMs: 120 }), true); });