diff --git a/TUICallKit-Vue3/tongji/components/VoiceSpeakMascot.vue b/TUICallKit-Vue3/tongji/components/VoiceSpeakMascot.vue new file mode 100644 index 00000000..44c2c16a --- /dev/null +++ b/TUICallKit-Vue3/tongji/components/VoiceSpeakMascot.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/TUICallKit-Vue3/tongji/composables/useSpeechToText.js b/TUICallKit-Vue3/tongji/composables/useSpeechToText.js index 09f01693..9b91d47a 100644 --- a/TUICallKit-Vue3/tongji/composables/useSpeechToText.js +++ b/TUICallKit-Vue3/tongji/composables/useSpeechToText.js @@ -2,6 +2,27 @@ import { ref, onUnmounted } from 'vue' const MIN_HOLD_MS = 280 +/** 短按/过早松手时插件常见错误,不应打扰用户 */ +function isBenignSttError(msg, retcode) { + const m = String(msg || '').toLowerCase() + if (/internal voice data failed/i.test(m)) return true + if (/voice data failed/i.test(m)) return true + if (/recordfailed|record failed|record manager/i.test(m)) return true + if (/please stop after start/i.test(m)) return true + if (/no recognition|not start|未开始|无识别/i.test(m)) return true + const code = Number(retcode) + if (code === -30002 || code === -30003 || code === -30012) return true + return false +} + +function friendlySttError(msg) { + const m = String(msg || '').trim() + if (!m || isBenignSttError(m)) return '' + if (/not allowed|auth|permission|权限|麦克风/i.test(m)) return '请允许使用麦克风' + if (/network|网络|timeout/i.test(m)) return '网络异常,请稍后再试' + return '语音识别失败,请重试' +} + // #ifdef MP-WEIXIN // WechatSI 录音管理器是全局单例:回调只在模块级绑定一次, // 再分发给「当前活跃实例」。否则跨页面(如 more → weekly)后, @@ -112,7 +133,8 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo function stopListening() { // #ifdef MP-WEIXIN const ownsRecording = sharedRecording && activeCtl === controller - if (wxRecordManager && (sttListening.value || startRequested || ownsRecording)) { + // 仅在识别已真正开始后再 stop,避免 -30012 / internal voice data failed + if (wxRecordManager && (sttListening.value || ownsRecording)) { try { wxRecordManager.stop() } catch (e) {} @@ -128,6 +150,7 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo // #endif startRequested = false + sttHolding.value = false sttListening.value = false } @@ -171,7 +194,6 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo } try { startRequested = true - sharedRecording = true wxRecordManager.start({ duration: 60000, lang: 'zh_CN' @@ -251,22 +273,28 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo // #endif } - /** 松手结束:手指抬起 */ - function endHoldSpeech() { + /** 松手结束:手指抬起;force=true 时强制取消尚未真正开始的按住会话 */ + function endHoldSpeech(force = false) { let recording = false // #ifdef MP-WEIXIN recording = sharedRecording && activeCtl === controller // #endif - if (!sttHolding.value && !sttListening.value && !startRequested && !recording) { + if (!force && !sttHolding.value && !sttListening.value && !startRequested && !recording) { return } sttHolding.value = false // 松手后不再补开排队的录音 pendingStartSession = 0 - if (sttListening.value || startRequested || recording) { + if (sttListening.value || recording) { stopListening() return } + if (startRequested) { + // start 已发出但 onStart 未到:取消会话,勿调 stop() + startRequested = false + holdSessionId += 1 + return + } holdSessionId += 1 } @@ -276,9 +304,9 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo const controller = { handleStart() { if (!sttHolding.value) { - try { - wxRecordManager.stop() - } catch (e) {} + // 用户已松手,忽略迟到的 onStart,不再 stop() 以免触发插件报错 + startRequested = false + sttListening.value = false return } startRequested = false @@ -306,6 +334,7 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo sttListening.value = false startRequested = false const msg = (res && res.msg) || '' + const retcode = res && (res.retcode ?? res.errCode ?? res.code) // 上一段未停止就再次 start 触发:停止后若仍按住则自动重试,不打扰用户 if (/please stop after start/i.test(msg)) { try { @@ -316,12 +345,18 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo } pendingStartSession = 0 sttHolding.value = false + const heldMs = Date.now() - holdStartTs + if (isBenignSttError(msg, retcode) || heldMs < MIN_HOLD_MS) { + if (typeof onError === 'function') onError(msg || '') + return + } // 上层(如一问一答流程)可接管错误并自行重试,返回 true 时不再弹默认提示 if (typeof onError === 'function') { const handled = onError(msg || '') if (handled === true) return } - notify(msg || '语音识别失败') + const tip = friendlySttError(msg) + if (tip) notify(tip) } } @@ -411,3 +446,5 @@ export function useSpeechToText({ onResult, onSettle, onPartial, onError, showTo stopSpeechToText: endHoldSpeech } } + +export { isBenignSttError, friendlySttError } diff --git a/TUICallKit-Vue3/tongji/pages/more.vue b/TUICallKit-Vue3/tongji/pages/more.vue index 6cfa97c8..1ee00225 100644 --- a/TUICallKit-Vue3/tongji/pages/more.vue +++ b/TUICallKit-Vue3/tongji/pages/more.vue @@ -1,5 +1,5 @@