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:
@@ -147,6 +147,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user