chore:语音输入有返回
This commit is contained in:
@@ -244,7 +244,7 @@ const localDevConfig = ({
|
||||
// })['2811']; // 本地
|
||||
// })['5g-317']; // 电磁兼容
|
||||
// })['5g-351']; //
|
||||
})['1']; // 本地
|
||||
})['2881']; // 本地
|
||||
// })['local-2']; // 本地
|
||||
// })['local-11']; // 本地
|
||||
// })['1-test']; // test
|
||||
|
||||
@@ -181,19 +181,27 @@ export function createVoiceRecorder(opts = {}) {
|
||||
class VoiceRecorder {
|
||||
constructor(opts) {
|
||||
this._maxDuration = opts.maxDuration || DEFAULT_MAX_DURATION_MS
|
||||
this._format = opts.format || 'mp3'
|
||||
// PR-FRONTEND-VOICE-FORMAT (2026-09-29): 微信小程序在部分 Android 机型上对 mp3 兼容性差,
|
||||
// 表现为一启动就 onStop(duration=0,fileSize=0)。aac 是微信录音默认/最稳格式。
|
||||
this._format = opts.format || 'aac'
|
||||
this._sampleRate = opts.sampleRate || 16000
|
||||
this._channels = opts.numberOfChannels || 1
|
||||
this._bitRate = opts.encodeBitRate || 48000
|
||||
|
||||
this._listeners = { progress: [], error: [] }
|
||||
this._isRecording = false
|
||||
this._isStarting = false // start() 已调用, 但 onStart 尚未回调
|
||||
this._stopResolve = null // 当前 stop() 等待 onStop 解析的 resolve
|
||||
this._pendingStop = false // onStart 之前就调用了 stop() → 等开始后立即停
|
||||
this._startTs = 0
|
||||
this._id = Math.random().toString(36).slice(2, 8)
|
||||
this._autoStopTimer = null
|
||||
this._earlyStopResult = null // PR-FRONTEND-VOICE-RACE: onStop 在 stop() 之前触发时缓存结果
|
||||
|
||||
// 平台相关句柄
|
||||
this._h5 = null // { mediaRecorder, stream, audioCtx, analyser, chunks, mime, rafId }
|
||||
this._uniRec = null // uni.getRecorderManager() 返回值
|
||||
console.log(`[voice-recorder:${this._id}] created`)
|
||||
}
|
||||
|
||||
on(event, cb) {
|
||||
@@ -226,6 +234,9 @@ class VoiceRecorder {
|
||||
if (this._isRecording) {
|
||||
return Promise.reject(_err('ALREADY_STARTED', 'recorder already started'))
|
||||
}
|
||||
if (this._isStarting) {
|
||||
return Promise.reject(_err('ALREADY_STARTED', 'recorder is starting'))
|
||||
}
|
||||
|
||||
if (isH5()) {
|
||||
return this._startH5()
|
||||
@@ -241,7 +252,29 @@ class VoiceRecorder {
|
||||
* resolve(null) 表示已取消 / 无产物,调用方应忽略。
|
||||
*/
|
||||
stop() {
|
||||
if (!this._isRecording) return Promise.resolve(null)
|
||||
console.log(`[voice-recorder:${this._id}] stop() called`, { isRecording: this._isRecording, isStarting: this._isStarting, hasUniRec: !!this._uniRec, hasStopResolve: !!this._stopResolve, earlyResult: !!this._earlyStopResult })
|
||||
if (!this._isRecording) {
|
||||
// PR-FRONTEND-VOICE-RACE (2026-09-29): 真机常见"按住即松手"——
|
||||
// onStart 尚未回调 (录音未真正开始) 就调用 stop(), 原逻辑直接返回 null
|
||||
// 导致上层判定"录音失败,请重试"。这里改为挂起, 等 onStart 触发后立即停,
|
||||
// 产物仍走 onStop 正常返回 (太短的音轨后端可自行拒绝)。
|
||||
if (this._isStarting && this._uniRec) {
|
||||
console.log(`[voice-recorder:${this._id}] stop() deferred: waiting for onStart`)
|
||||
return new Promise((resolve) => {
|
||||
this._pendingStop = true
|
||||
this._stopResolve = resolve
|
||||
})
|
||||
}
|
||||
// onStop 在 stop() 之前触发, 且有有效产物则返回缓存结果
|
||||
if (this._earlyStopResult) {
|
||||
const r = this._earlyStopResult
|
||||
this._earlyStopResult = null
|
||||
console.log(`[voice-recorder:${this._id}] stop() returning early onStop result`)
|
||||
return Promise.resolve(r)
|
||||
}
|
||||
console.log(`[voice-recorder:${this._id}] stop() early return null (not recording)`)
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
if (isH5()) return this._stopH5()
|
||||
return this._stopUni()
|
||||
}
|
||||
@@ -444,46 +477,68 @@ class VoiceRecorder {
|
||||
}
|
||||
|
||||
this._uniRec = rec
|
||||
this._isStarting = true
|
||||
console.log(`[voice-recorder:${this._id}] _startUni: start() invoked on singleton`)
|
||||
|
||||
rec.onStart(() => {
|
||||
console.log(`[voice-recorder:${this._id}] onStart fired`)
|
||||
this._isRecording = true
|
||||
this._isStarting = false
|
||||
this._startTs = Date.now()
|
||||
this._setAutoStopTimer()
|
||||
if (this._pendingStop) {
|
||||
// 用户在录音真正开始前就松手: 立即结束, 产物经 onStop 返回
|
||||
console.log(`[voice-recorder:${this._id}] onStart: pendingStop detected, calling rec.stop()`)
|
||||
this._pendingStop = false
|
||||
try { rec.stop() } catch (e) { /* onStop 自会兜底 */ }
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
|
||||
rec.onStop((res) => {
|
||||
// res = { tempFilePath, duration, fileSize }
|
||||
console.log(`[voice-recorder:${this._id}] onStop fired`, res, 'hasStopResolve=', !!this._stopResolve)
|
||||
this._uniRec = null
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
const mime = this._format === 'wav' ? 'audio/wav'
|
||||
: this._format === 'aac' ? 'audio/aac'
|
||||
: 'audio/mpeg'
|
||||
if (this._uniResolve) {
|
||||
const r = this._uniResolve
|
||||
this._uniResolve = null
|
||||
r({
|
||||
filePath: res.tempFilePath,
|
||||
blob: null,
|
||||
fileName: '',
|
||||
duration: typeof res.duration === 'number' ? res.duration : (Date.now() - this._startTs),
|
||||
mime,
|
||||
size: res.fileSize || 0,
|
||||
cancelled: false
|
||||
})
|
||||
// PR-FRONTEND-VOICE-DURATION-FIX (2026-09-29): 微信 onStop 的 duration
|
||||
// 偶尔为 0, 用 start→stop 真实时长兜底, 避免下游按 duration=0 丢弃。
|
||||
const realDuration = this._startTs ? Date.now() - this._startTs : 0
|
||||
const reportedDuration = (res && typeof res.duration === 'number') ? res.duration : 0
|
||||
const result = {
|
||||
filePath: res.tempFilePath,
|
||||
blob: null,
|
||||
fileName: '',
|
||||
duration: reportedDuration > 0 ? reportedDuration : realDuration,
|
||||
mime,
|
||||
size: res.fileSize || 0,
|
||||
cancelled: false
|
||||
}
|
||||
if (this._stopResolve) {
|
||||
const r = this._stopResolve
|
||||
this._stopResolve = null
|
||||
r(result)
|
||||
} else if (result.filePath && result.duration > 0) {
|
||||
// PR-FRONTEND-VOICE-RACE: onStop 在 stop() 之前触发且产物有效, 缓存起来
|
||||
this._earlyStopResult = result
|
||||
}
|
||||
})
|
||||
|
||||
rec.onError((err) => {
|
||||
const e = _err('UNI_RECORD_ERROR', (err && (err.errMsg || err.message)) || 'record failed')
|
||||
console.error(`[voice-recorder:${this._id}] UNI_RECORD_ERROR raw:`, err, 'errMsg=', err && err.errMsg, 'message=', err && err.message)
|
||||
this._uniRec = null
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
this._emit('error', e)
|
||||
if (this._uniResolve) {
|
||||
const r = this._uniResolve
|
||||
this._uniResolve = null
|
||||
if (this._stopResolve) {
|
||||
const r = this._stopResolve
|
||||
this._stopResolve = null
|
||||
r(null)
|
||||
} else {
|
||||
// 启动阶段的 reject
|
||||
@@ -530,25 +585,39 @@ class VoiceRecorder {
|
||||
|
||||
_stopUni() {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[voice-recorder:${this._id}] _stopUni called`)
|
||||
const rec = this._uniRec
|
||||
if (!rec) { resolve(null); return }
|
||||
this._uniResolve = resolve
|
||||
this._earlyStopResult = null
|
||||
this._stopResolve = resolve
|
||||
try { rec.stop() } catch (e) { resolve(null) }
|
||||
})
|
||||
}
|
||||
|
||||
_cancelUni() {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[voice-recorder:${this._id}] _cancelUni called`)
|
||||
this._earlyStopResult = null
|
||||
const rec = this._uniRec
|
||||
if (!rec) {
|
||||
// 挂起的 stop 需要一并了结, 避免松手后卡在 pending
|
||||
if (this._stopResolve && this._pendingStop) {
|
||||
const r = this._stopResolve
|
||||
this._stopResolve = null
|
||||
this._pendingStop = false
|
||||
r(null)
|
||||
}
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
this._uniResolve = () => resolve()
|
||||
this._stopResolve = () => resolve()
|
||||
this._pendingStop = false
|
||||
try { rec.stop() } catch (e) {
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
resolve()
|
||||
}
|
||||
|
||||
@@ -4073,6 +4073,7 @@ _handleStreamError(msg) {
|
||||
// - touchend 提交产物 (取消则丢弃)
|
||||
// - H5 鼠标不会触发 touch, click 也走 onVoiceHoldEnd 兜底 (无录音产物就当没按住)
|
||||
onVoiceHoldStart(e) {
|
||||
console.log('[ai-chat] onVoiceHoldStart fired', this._voiceRecorder ? 'recorder already exists' : 'creating recorder')
|
||||
if (this._voiceRecorder) return
|
||||
this.voiceHolding = true
|
||||
this.voiceHoldCancelling = false
|
||||
@@ -4090,6 +4091,7 @@ _handleStreamError(msg) {
|
||||
}
|
||||
this._onHoldError = (err) => {
|
||||
console.warn('[ai-chat] voice hold error:', err)
|
||||
console.error('[ai-chat] voice hold error raw:', JSON.stringify(err), 'errMsg=', err && err.errMsg, 'message=', err && err.message)
|
||||
uni.showToast({ title: this._voiceErrMsg(err) || this.$lang('ai-chat.index.voiceInputFailed'), icon: 'none' })
|
||||
this._discardVoiceRecorder()
|
||||
}
|
||||
@@ -4119,6 +4121,7 @@ _handleStreamError(msg) {
|
||||
},
|
||||
|
||||
async onVoiceHoldEnd() {
|
||||
console.log('[ai-chat] onVoiceHoldEnd fired, voiceHolding=', this.voiceHolding, 'recorder=', !!this._voiceRecorder)
|
||||
if (!this.voiceHolding) {
|
||||
// 非按住触发的 click (H5 鼠标等), 不做任何事, 避免双触发
|
||||
return
|
||||
@@ -4129,6 +4132,7 @@ _handleStreamError(msg) {
|
||||
|
||||
if (!this._voiceRecorder) {
|
||||
// 启动录音器失败 (无 touch 触发的 click) → 退回弹窗路径, 保留功能不丢
|
||||
console.log('[ai-chat] onVoiceHoldEnd: no recorder, fallback to voice panel')
|
||||
this.showVoicePanel()
|
||||
return
|
||||
}
|
||||
@@ -4139,13 +4143,19 @@ _handleStreamError(msg) {
|
||||
}
|
||||
try {
|
||||
const result = await this._voiceRecorder.stop()
|
||||
console.log('[ai-chat] voice hold stop result:', JSON.stringify(result))
|
||||
this._teardownVoiceRecorder()
|
||||
if (!result || !result.filePath) {
|
||||
uni.showToast({ title: this.$lang('ai-chat.index.voiceStopFailed'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
await this._handleRecordedAudio(result)
|
||||
// PR-FRONTEND-VOICE-AUTO-SEND (2026-09-29): 按住说话像微信一样, 松手后自动发送,
|
||||
// 不需要用户再点一次发送按钮。
|
||||
console.log('[ai-chat] voice hold auto-send triggered')
|
||||
this.sendMessage()
|
||||
} catch (e) {
|
||||
console.error('[ai-chat] voice hold end error:', e && e.errMsg, e && e.message, JSON.stringify(e))
|
||||
this._teardownVoiceRecorder()
|
||||
uni.showToast({ title: this._voiceErrMsg(e) || this.$lang('ai-chat.index.voiceInputFailed'), icon: 'none' })
|
||||
}
|
||||
@@ -4153,6 +4163,7 @@ _handleStreamError(msg) {
|
||||
|
||||
// 按住说话: 手势被系统打断 (来电/滑动取消)
|
||||
onVoiceHoldCancel() {
|
||||
console.log('[ai-chat] onVoiceHoldCancel fired')
|
||||
if (!this.voiceHolding) return
|
||||
this.voiceHolding = false
|
||||
this.voiceHoldCancelling = false
|
||||
@@ -4267,12 +4278,14 @@ _handleStreamError(msg) {
|
||||
},
|
||||
|
||||
_discardVoiceRecorder() {
|
||||
console.log('[ai-chat] _discardVoiceRecorder called')
|
||||
if (!this._voiceRecorder) return
|
||||
try { this._voiceRecorder.cancel() } catch (e) { /* noop */ }
|
||||
this._teardownVoiceRecorder()
|
||||
},
|
||||
|
||||
_teardownVoiceRecorder() {
|
||||
console.log('[ai-chat] _teardownVoiceRecorder called')
|
||||
if (this._voiceRecorder) {
|
||||
if (this._onHoldProgress) this._voiceRecorder.off('progress', this._onHoldProgress)
|
||||
if (this._onHoldError) this._voiceRecorder.off('error', this._onHoldError)
|
||||
@@ -4297,7 +4310,8 @@ _handleStreamError(msg) {
|
||||
case 'ALREADY_STARTED':
|
||||
return this.$lang('ai-chat.index.voiceStartFailed')
|
||||
case 'UNI_RECORD_ERROR':
|
||||
return this.$lang('ai-chat.index.voiceStopFailed')
|
||||
// 诊断期: 直接透出微信原始错误 (errMsg/message), 便于截图定位根因
|
||||
return (err && (err.errMsg || err.message)) || this.$lang('ai-chat.index.voiceStopFailed')
|
||||
default:
|
||||
return err.message || ''
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ export default {
|
||||
}
|
||||
this._onError = (err) => {
|
||||
console.warn('[voice-panel] recorder error:', err)
|
||||
console.error('[voice-panel] recorder error raw:', JSON.stringify(err), 'errMsg=', err && err.errMsg, 'message=', err && err.message)
|
||||
uni.showToast({ title: this._voiceErrMsg(err) || this.$lang('ai-chat.index.voiceInputFailed'), icon: 'none' })
|
||||
this._discardRecorderIfAny()
|
||||
}
|
||||
@@ -227,7 +228,7 @@ export default {
|
||||
case 'ALREADY_STARTED':
|
||||
return this.$lang('ai-chat.index.voiceStartFailed')
|
||||
case 'UNI_RECORD_ERROR':
|
||||
return this.$lang('ai-chat.index.voiceStopFailed')
|
||||
return (err && (err.errMsg || err.message)) || this.$lang('ai-chat.index.voiceStopFailed')
|
||||
default:
|
||||
return err.message || ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user