diff --git a/packages/core/src/stores/tts-store.test.ts b/packages/core/src/stores/tts-store.test.ts index 4ae417ec..114bb052 100644 --- a/packages/core/src/stores/tts-store.test.ts +++ b/packages/core/src/stores/tts-store.test.ts @@ -28,6 +28,7 @@ function createMockPlayer(): MockTTSPlayer { }); player.stop = vi.fn(() => { player.onStateChange?.("stopped"); + player.onEnd?.(); }); return player; } @@ -235,4 +236,30 @@ describe("useTTSStore — re-speak on synth change (#370)", () => { expect(edgePlayer.speak).not.toHaveBeenCalled(); expect(systemPlayer.speak).not.toHaveBeenCalled(); }); + + it("[engine-switch] stops active playback without firing reader onEnd", () => { + const onEnd = vi.fn(); + startEdge(); + useTTSStore.getState().setOnEnd(onEnd); + edgePlayer.stop.mockClear(); + + useTTSStore.getState().updateConfig({ engine: "system" }); + + expect(edgePlayer.stop).toHaveBeenCalledOnce(); + expect(edgePlayer.onEnd).toBeUndefined(); + expect(onEnd).not.toHaveBeenCalled(); + expect(useTTSStore.getState().playState).toBe("stopped"); + expect(useTTSStore.getState().config.engine).toBe("system"); + }); + + it("[engine-switch] does not stop playback for non-engine synth updates", () => { + startEdge(); + edgePlayer.stop.mockClear(); + + useTTSStore.getState().updateConfig({ rate: 1.3 }); + + expect(edgePlayer.stop).not.toHaveBeenCalled(); + expect(useTTSStore.getState().playState).toBe("playing"); + expect(useTTSStore.getState().config.rate).toBe(1.3); + }); }); diff --git a/packages/core/src/stores/tts-store.ts b/packages/core/src/stores/tts-store.ts index cee1e02f..b28e5e9a 100644 --- a/packages/core/src/stores/tts-store.ts +++ b/packages/core/src/stores/tts-store.ts @@ -373,10 +373,19 @@ export const useTTSStore = create()( updateConfig: (updates) => { const previousConfig = normalizeTTSConfig(get().config); const nextConfig = normalizeTTSConfig({ ...previousConfig, ...updates }); + const engineChanged = + updates.engine !== undefined && nextConfig.engine !== previousConfig.engine; + const wasPlaying = isActivePlay(get().playState); set({ config: nextConfig }); - // [占位 · #427/#349] engine 变化 + 播放中 → 停播。合并 #427 时在此插入, - // 并在该分支内调用 clearRespeakTimer()。 + if (engineChanged && wasPlaying) { + clearRespeakTimer(); + _sessionGeneration += 1; + detachAndStopAllPlayers(); + _dashscopeActiveVoice = undefined; + set({ playState: "stopped" }); + return; + } if ( shouldRespeakForSynthChange(previousConfig, nextConfig) &&