From 07b81f57a6ee78928747fbd1716bf0482bc85aef Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Sun, 5 Jul 2026 13:38:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=9D=A2=E8=AF=95=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=97=B6=E6=9C=BA=20+=20=E5=BD=95=E9=9F=B3=E5=BC=80=E5=A7=8B?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=AE=88=E5=8D=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题1(录音失败): recorder.start() 在 uni.authorize 异步回调里调用, 用户松手触发 stopRecord 时录音还没实际开始,调 recorder.stop() 报错。 修复: 增加 recordingStarted 标记,onsStart 回调设为 true,stopRecord 检查该标记,若录音未开始直接跳过。 问题2(开始被当答案): - onMounted 不再自动调用 startInterview() 创建面试,等待用户首次发送 - greeting 文案改为准备好后发送任意消息,我会立即开始面试并给出第一个问题 - sendAnswer 首次发送: 用户消息仅显示在本地对话中,调用 startInterview() 创建面试并追加第一个问题,不提交答案到 /interview/{id}/answer - startInterview 不再替换 messages,改为 push AI 消息追加到现有对话 - selectPosition 同理,不再立即 startInterview,等用户发消息 --- zhiyin-app/src/pages/interview/interview.vue | 35 +++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/zhiyin-app/src/pages/interview/interview.vue b/zhiyin-app/src/pages/interview/interview.vue index 7427d7d..fb95bb1 100644 --- a/zhiyin-app/src/pages/interview/interview.vue +++ b/zhiyin-app/src/pages/interview/interview.vue @@ -146,7 +146,8 @@ const aiAmplitudeData = ref([]) const isSpeaking = ref(false) const dhRef = ref(null) const isRecording = ref(false) - let recorder = null +let recorder = null +let recordingStarted = false // 标记 recorder.start() 是否已实际调用,避免 stopRecord 在 start 前触发 // 录音初始化(一次性创建 recorder 并设置事件) function initRecorder() { @@ -154,15 +155,18 @@ function initRecorder() { if (recorder) return // 已初始化 recorder = uni.getRecorderManager() recorder.onStart(() => { + recordingStarted = true console.log('[Recorder] start recording') }) recorder.onError((err) => { console.error('[Recorder] error:', err) isRecording.value = false + recordingStarted = false uni.showToast({ title: '录音失败,请重试', icon: 'none' }) }) recorder.onStop(async (res) => { isRecording.value = false + recordingStarted = false console.log('[Recorder] stop, tempFilePath:', res.tempFilePath) if (!res.tempFilePath) { uni.showToast({ title: '录音文件为空', icon: 'none' }) @@ -207,7 +211,7 @@ onLoad((options) => { if (options?.position) { const pos = decodeURIComponent(options.position) position.value = pos - messages.value = [{ role: 'ai', content: `你好!我是你的专属 ${pos} 面试官,准备好了就开始吧!` }] + messages.value = [{ role: 'ai', content: `你好!我是你的专属 ${pos} 面试官。准备好后发送任意消息,我会立即开始面试并给出第一个问题。` }] } }) @@ -229,8 +233,7 @@ const loadPositions = async () => { const selectPosition = (pos) => { position.value = pos.name showPositionPicker.value = false - messages.value = [{ role: 'ai', content: `你好!我是你的专属 ${pos.name} 面试官,准备好了就开始吧!` }] - startInterview() + messages.value = [{ role: 'ai', content: `你好!我是你的专属 ${pos.name} 面试官。准备好后发送任意消息,我会立即开始面试并给出第一个问题。` }] } onMounted(() => { @@ -239,8 +242,6 @@ onMounted(() => { if (!position.value) { loadPositions() showPositionPicker.value = true - } else if (token()) { - startInterview() } }) @@ -270,9 +271,13 @@ const startInterview = async () => { }) if (res.statusCode >= 200 && res.statusCode < 300 && res.data) { interviewId.value = res.data.id - messages.value = res.data.messages || messages.value answeredCount.value = res.data.questionCount || 0 if (res.data.totalQuestions) MAX_QUESTIONS = res.data.totalQuestions + // 将后端返回的 AI 消息追加入对话,不替换已有消息(保留用户已发的准备消息) + if (res.data.messages?.length) { + const aiMsgs = res.data.messages.filter(m => m.role === 'ai') + if (aiMsgs.length > 0) messages.value.push(...aiMsgs) + } // Speak first question in avatar mode if (avatarMode.value && res.data.messages?.length) { const last = res.data.messages[res.data.messages.length - 1] @@ -301,13 +306,18 @@ const startInterview = async () => { const sendAnswer = async () => { if (!inputText.value.trim() || aiLoading.value || isComplete.value) return if (!token()) { checkLogin(); return } - // 确保面试已创建(startInterview 是 async,必须 await) + const answer = inputText.value.trim() + + // 首次发送:不把用户消息当答案提交,而是先创建面试获取第一个问题 if (!interviewId.value) { + messages.value.push({ role: 'user', content: answer }) + inputText.value = '' + scrollToBottom() await startInterview() - if (!interviewId.value) return // 创建失败,不丢弃用户输入 + // startInterview 成功后已经把第一个问题追加到 messages 了 + return } - const answer = inputText.value.trim() messages.value.push({ role: 'user', content: answer }) inputText.value = '' scrollToBottom() @@ -402,7 +412,7 @@ function startRecord() { if (aiLoading.value || isComplete.value) return // #ifdef MP-WEIXIN if (isRecording.value) return - // 先标记正在录音,避免 touchend 在 authorize 回调前就触发 stopRecord + recordingStarted = false isRecording.value = true uni.authorize({ scope: 'scope.record', @@ -413,7 +423,6 @@ function startRecord() { uni.showToast({ title: '录音初始化失败', icon: 'none' }) return } - // 确保在授权通过后才开始录音,避免 stopRecord 先于 startRecord 被调用 recorder.start({ format: 'wav', sampleRate: 16000, @@ -442,6 +451,8 @@ function startRecord() { function stopRecord() { if (!isRecording.value) return isRecording.value = false + // 松手太快,recorder.start() 还没实际执行(authorize 异步或录音尚未开始),直接跳过 + if (!recordingStarted) return if (!recorder) return recorder.stop() }