fix: 面试创建时机 + 录音开始状态守卫

问题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,等用户发消息
This commit is contained in:
yuzhiran
2026-07-05 13:38:20 +08:00
parent 1422c04b2c
commit 07b81f57a6
+23 -12
View File
@@ -146,7 +146,8 @@ const aiAmplitudeData = ref([])
const isSpeaking = ref(false) const isSpeaking = ref(false)
const dhRef = ref(null) const dhRef = ref(null)
const isRecording = ref(false) const isRecording = ref(false)
let recorder = null let recorder = null
let recordingStarted = false // 标记 recorder.start() 是否已实际调用,避免 stopRecord 在 start 前触发
// 录音初始化(一次性创建 recorder 并设置事件) // 录音初始化(一次性创建 recorder 并设置事件)
function initRecorder() { function initRecorder() {
@@ -154,15 +155,18 @@ function initRecorder() {
if (recorder) return // 已初始化 if (recorder) return // 已初始化
recorder = uni.getRecorderManager() recorder = uni.getRecorderManager()
recorder.onStart(() => { recorder.onStart(() => {
recordingStarted = true
console.log('[Recorder] start recording') console.log('[Recorder] start recording')
}) })
recorder.onError((err) => { recorder.onError((err) => {
console.error('[Recorder] error:', err) console.error('[Recorder] error:', err)
isRecording.value = false isRecording.value = false
recordingStarted = false
uni.showToast({ title: '录音失败,请重试', icon: 'none' }) uni.showToast({ title: '录音失败,请重试', icon: 'none' })
}) })
recorder.onStop(async (res) => { recorder.onStop(async (res) => {
isRecording.value = false isRecording.value = false
recordingStarted = false
console.log('[Recorder] stop, tempFilePath:', res.tempFilePath) console.log('[Recorder] stop, tempFilePath:', res.tempFilePath)
if (!res.tempFilePath) { if (!res.tempFilePath) {
uni.showToast({ title: '录音文件为空', icon: 'none' }) uni.showToast({ title: '录音文件为空', icon: 'none' })
@@ -207,7 +211,7 @@ onLoad((options) => {
if (options?.position) { if (options?.position) {
const pos = decodeURIComponent(options.position) const pos = decodeURIComponent(options.position)
position.value = pos 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) => { const selectPosition = (pos) => {
position.value = pos.name position.value = pos.name
showPositionPicker.value = false showPositionPicker.value = false
messages.value = [{ role: 'ai', content: `你好!我是你的专属 ${pos.name} 面试官准备好了就开始吧!` }] messages.value = [{ role: 'ai', content: `你好!我是你的专属 ${pos.name} 面试官准备好后发送任意消息,我会立即开始面试并给出第一个问题。` }]
startInterview()
} }
onMounted(() => { onMounted(() => {
@@ -239,8 +242,6 @@ onMounted(() => {
if (!position.value) { if (!position.value) {
loadPositions() loadPositions()
showPositionPicker.value = true showPositionPicker.value = true
} else if (token()) {
startInterview()
} }
}) })
@@ -270,9 +271,13 @@ const startInterview = async () => {
}) })
if (res.statusCode >= 200 && res.statusCode < 300 && res.data) { if (res.statusCode >= 200 && res.statusCode < 300 && res.data) {
interviewId.value = res.data.id interviewId.value = res.data.id
messages.value = res.data.messages || messages.value
answeredCount.value = res.data.questionCount || 0 answeredCount.value = res.data.questionCount || 0
if (res.data.totalQuestions) MAX_QUESTIONS = res.data.totalQuestions 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 // Speak first question in avatar mode
if (avatarMode.value && res.data.messages?.length) { if (avatarMode.value && res.data.messages?.length) {
const last = res.data.messages[res.data.messages.length - 1] const last = res.data.messages[res.data.messages.length - 1]
@@ -301,13 +306,18 @@ const startInterview = async () => {
const sendAnswer = async () => { const sendAnswer = async () => {
if (!inputText.value.trim() || aiLoading.value || isComplete.value) return if (!inputText.value.trim() || aiLoading.value || isComplete.value) return
if (!token()) { checkLogin(); return } if (!token()) { checkLogin(); return }
// 确保面试已创建(startInterview 是 async,必须 await const answer = inputText.value.trim()
// 首次发送:不把用户消息当答案提交,而是先创建面试获取第一个问题
if (!interviewId.value) { if (!interviewId.value) {
messages.value.push({ role: 'user', content: answer })
inputText.value = ''
scrollToBottom()
await startInterview() await startInterview()
if (!interviewId.value) return // 创建失败,不丢弃用户输入 // startInterview 成功后已经把第一个问题追加到 messages 了
return
} }
const answer = inputText.value.trim()
messages.value.push({ role: 'user', content: answer }) messages.value.push({ role: 'user', content: answer })
inputText.value = '' inputText.value = ''
scrollToBottom() scrollToBottom()
@@ -402,7 +412,7 @@ function startRecord() {
if (aiLoading.value || isComplete.value) return if (aiLoading.value || isComplete.value) return
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
if (isRecording.value) return if (isRecording.value) return
// 先标记正在录音,避免 touchend 在 authorize 回调前就触发 stopRecord recordingStarted = false
isRecording.value = true isRecording.value = true
uni.authorize({ uni.authorize({
scope: 'scope.record', scope: 'scope.record',
@@ -413,7 +423,6 @@ function startRecord() {
uni.showToast({ title: '录音初始化失败', icon: 'none' }) uni.showToast({ title: '录音初始化失败', icon: 'none' })
return return
} }
// 确保在授权通过后才开始录音,避免 stopRecord 先于 startRecord 被调用
recorder.start({ recorder.start({
format: 'wav', format: 'wav',
sampleRate: 16000, sampleRate: 16000,
@@ -442,6 +451,8 @@ function startRecord() {
function stopRecord() { function stopRecord() {
if (!isRecording.value) return if (!isRecording.value) return
isRecording.value = false isRecording.value = false
// 松手太快,recorder.start() 还没实际执行(authorize 异步或录音尚未开始),直接跳过
if (!recordingStarted) return
if (!recorder) return if (!recorder) return
recorder.stop() recorder.stop()
} }