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 && (