From fa640ca5375ca56c7b42e7e293b73dab95975b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=BA=94?= Date: Sat, 4 Jul 2026 16:23:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(voice):=20CloneModal=20=E5=BD=95=E9=9F=B3?= =?UTF-8?q?=205=20=E5=88=86=E9=92=9F=E4=B8=8A=E9=99=90=20+=20cloneCounter?= =?UTF-8?q?=20=E6=94=B9=20useRef?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P3 优化(代码审计建议): 1. 录音达到 5 分钟自动停止并提示用户,避免传超大文件 2. cloneCounter 从模块级变量改为组件内 useRef,避免多实例串号 Co-Authored-By: Claude Fable 5 --- apps/web/src/components/voice/CloneModal.tsx | 44 ++++++++++++++------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/voice/CloneModal.tsx b/apps/web/src/components/voice/CloneModal.tsx index 1107862e8..29d4d0b81 100644 --- a/apps/web/src/components/voice/CloneModal.tsx +++ b/apps/web/src/components/voice/CloneModal.tsx @@ -44,15 +44,8 @@ const ACCEPTED_EXTENSIONS = ["mp3", "wav", "m4a"]; const ACCEPTED_MIME = ".mp3,.wav,.m4a,audio/mpeg,audio/wav,audio/mp4"; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB -/* ── 默认音色名称计数器 ─────────────────────────────────── */ - -let cloneCounter = 1; - -const getNextDefaultName = (): string => { - const name = `我的声音 ${cloneCounter}`; - cloneCounter += 1; - return name; -}; +/** 最长录制时长:5 分钟(秒) */ +const MAX_RECORD_SECONDS = 5 * 60; /* ── 组件 ───────────────────────────────────────────────── */ @@ -78,6 +71,15 @@ const CloneModal: React.FC = ({ const recordTimerRef = useRef | null>(null); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); + /** 默认音色名称计数器(组件级 ref,避免多实例串号) */ + const cloneCounterRef = useRef(1); + + /** 生成下一个默认音色名称 */ + const getNextDefaultName = useCallback((): string => { + const name = `我的声音 ${cloneCounterRef.current}`; + cloneCounterRef.current += 1; + return name; + }, []); /** 组件卸载时清理定时器和 MediaRecorder */ useEffect(() => { @@ -110,7 +112,7 @@ const CloneModal: React.FC = ({ mediaRecorderRef.current.stop(); } mediaRecorderRef.current = null; - }, []); + }, [getNextDefaultName]); /** 关闭弹窗 */ const handleClose = useCallback(() => { @@ -238,7 +240,25 @@ const CloneModal: React.FC = ({ setErrorMessage(""); recordTimerRef.current = setInterval(() => { - setRecordTime((prev) => prev + 1); + setRecordTime((prev) => { + const next = prev + 1; + if (next >= MAX_RECORD_SECONDS) { + // 达到 5 分钟上限,自动停止录制 + setTimeout(() => { + setIsRecording(false); + if (recordTimerRef.current) { + clearInterval(recordTimerRef.current); + recordTimerRef.current = null; + } + if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + mediaRecorderRef.current.stop(); + } + setErrorMessage("已达最长录制时长(5分钟),已自动停止"); + }, 0); + return MAX_RECORD_SECONDS; + } + return next; + }); }, 1000); } catch { setErrorMessage("无法访问麦克风,请检查浏览器权限设置"); @@ -446,7 +466,7 @@ const CloneModal: React.FC = ({ ? `录制中 ${formatRecordTime(recordTime)}` : recordedBlob ? `已录制 ${formatRecordTime(recordTime)}` - : "点击按钮开始录制你的声音"} + : "点击按钮开始录制(最长 5 分钟)"}

{isRecording && (