feat(mini): add voice recording compatibility logging
This commit is contained in:
@@ -53,6 +53,135 @@ function formatAudioDuration(durationMs?: number | null): string {
|
|||||||
return `${totalSeconds}s`
|
return `${totalSeconds}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VoiceRuntimeInfo = {
|
||||||
|
platform: string
|
||||||
|
system: string
|
||||||
|
brand: string
|
||||||
|
model: string
|
||||||
|
version: string
|
||||||
|
SDKVersion: string
|
||||||
|
isIOS: boolean
|
||||||
|
isAndroid: boolean
|
||||||
|
isHarmony: boolean
|
||||||
|
canUseRecorderManager: boolean
|
||||||
|
canUseInnerAudioContext: boolean
|
||||||
|
supportsMp3Record: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordProfile = {
|
||||||
|
format: 'mp3' | 'aac'
|
||||||
|
mimeType: string
|
||||||
|
sampleRate: number
|
||||||
|
encodeBitRate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_RECORD_PROFILE: RecordProfile = {
|
||||||
|
format: 'mp3',
|
||||||
|
mimeType: 'audio/mpeg',
|
||||||
|
sampleRate: 16000,
|
||||||
|
encodeBitRate: 96000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMPAT_RECORD_PROFILE: RecordProfile = {
|
||||||
|
format: 'aac',
|
||||||
|
mimeType: 'audio/aac',
|
||||||
|
sampleRate: 16000,
|
||||||
|
encodeBitRate: 64000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FALLBACK_RUNTIME_INFO: VoiceRuntimeInfo = {
|
||||||
|
platform: '',
|
||||||
|
system: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
version: '',
|
||||||
|
SDKVersion: '',
|
||||||
|
isIOS: false,
|
||||||
|
isAndroid: false,
|
||||||
|
isHarmony: false,
|
||||||
|
canUseRecorderManager: false,
|
||||||
|
canUseInnerAudioContext: false,
|
||||||
|
supportsMp3Record: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersion(left: string, right: string): number {
|
||||||
|
const leftParts = left.split('.').map((item) => Number(item) || 0)
|
||||||
|
const rightParts = right.split('.').map((item) => Number(item) || 0)
|
||||||
|
const length = Math.max(leftParts.length, rightParts.length)
|
||||||
|
for (let index = 0; index < length; index += 1) {
|
||||||
|
const diff = (leftParts[index] || 0) - (rightParts[index] || 0)
|
||||||
|
if (diff !== 0) return diff
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCanIUse(apiName: string): boolean {
|
||||||
|
try {
|
||||||
|
if (typeof Taro.canIUse !== 'function') return false
|
||||||
|
return Boolean(Taro.canIUse(apiName))
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readVoiceRuntimeInfo(): VoiceRuntimeInfo {
|
||||||
|
let systemInfo: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
systemInfo = Taro.getSystemInfoSync() as Record<string, any>
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[voice-runtime] getSystemInfoSync failed', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = String(systemInfo.platform || '').toLowerCase()
|
||||||
|
const system = String(systemInfo.system || '')
|
||||||
|
const brand = String(systemInfo.brand || '')
|
||||||
|
const model = String(systemInfo.model || '')
|
||||||
|
const version = String(systemInfo.version || '')
|
||||||
|
const SDKVersion = String(systemInfo.SDKVersion || '')
|
||||||
|
const systemText = `${system} ${brand} ${model}`.toLowerCase()
|
||||||
|
const isHarmony = systemText.includes('harmony') || systemText.includes('openharmony') || systemText.includes('鸿蒙')
|
||||||
|
const isIOS = platform === 'ios' || systemText.includes('ios')
|
||||||
|
const isAndroid = platform === 'android' || systemText.includes('android') || isHarmony
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform,
|
||||||
|
system,
|
||||||
|
brand,
|
||||||
|
model,
|
||||||
|
version,
|
||||||
|
SDKVersion,
|
||||||
|
isIOS,
|
||||||
|
isAndroid,
|
||||||
|
isHarmony,
|
||||||
|
canUseRecorderManager: typeof Taro.getRecorderManager === 'function',
|
||||||
|
canUseInnerAudioContext: typeof Taro.createInnerAudioContext === 'function',
|
||||||
|
supportsMp3Record: !SDKVersion || compareVersion(SDKVersion, '2.6.0') >= 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreferredRecordProfile(runtime: VoiceRuntimeInfo): RecordProfile {
|
||||||
|
if (runtime.isHarmony || !runtime.supportsMp3Record) return COMPAT_RECORD_PROFILE
|
||||||
|
return DEFAULT_RECORD_PROFILE
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAudioUrlDebugInfo(rawUrl: string) {
|
||||||
|
const protocolMatch = rawUrl.match(/^([a-z]+):\/\//i)
|
||||||
|
let host = ''
|
||||||
|
try {
|
||||||
|
const match = rawUrl.match(/^[a-z]+:\/\/([^/?#]+)/i)
|
||||||
|
host = match?.[1] || ''
|
||||||
|
} catch {
|
||||||
|
host = ''
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rawUrl,
|
||||||
|
protocol: protocolMatch?.[1]?.toLowerCase() || '',
|
||||||
|
host,
|
||||||
|
isHttp: rawUrl.startsWith('http://'),
|
||||||
|
isHttps: rawUrl.startsWith('https://'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatDetail() {
|
export default function ChatDetail() {
|
||||||
const systemBanner = useSystemBanner()
|
const systemBanner = useSystemBanner()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -99,21 +228,61 @@ export default function ChatDetail() {
|
|||||||
const activeConversationIdRef = useRef(initialConversationId)
|
const activeConversationIdRef = useRef(initialConversationId)
|
||||||
const latestLoadChatDataRef = useRef<(conversationId: number) => Promise<void>>(async () => {})
|
const latestLoadChatDataRef = useRef<(conversationId: number) => Promise<void>>(async () => {})
|
||||||
const recordingChildIdRef = useRef<number | null>(null)
|
const recordingChildIdRef = useRef<number | null>(null)
|
||||||
|
const recordProfileRef = useRef<RecordProfile>(DEFAULT_RECORD_PROFILE)
|
||||||
|
const runtimeInfoRef = useRef<VoiceRuntimeInfo>(FALLBACK_RUNTIME_INFO)
|
||||||
|
const recordStartedAtRef = useRef<number>(0)
|
||||||
|
const playingMessageIdRef = useRef<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
runtimeInfoRef.current = readVoiceRuntimeInfo()
|
||||||
|
recordProfileRef.current = getPreferredRecordProfile(runtimeInfoRef.current)
|
||||||
|
console.info('[voice-runtime] detected', {
|
||||||
|
...runtimeInfoRef.current,
|
||||||
|
preferredRecordProfile: recordProfileRef.current,
|
||||||
|
canIUse: {
|
||||||
|
getRecorderManager: getCanIUse('getRecorderManager'),
|
||||||
|
createInnerAudioContext: getCanIUse('createInnerAudioContext'),
|
||||||
|
uploadFile: getCanIUse('uploadFile'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof Taro.createInnerAudioContext !== 'function') {
|
||||||
|
console.warn('[audio] InnerAudioContext unavailable', runtimeInfoRef.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const audioContext = Taro.createInnerAudioContext()
|
const audioContext = Taro.createInnerAudioContext()
|
||||||
audioContextRef.current = audioContext
|
audioContextRef.current = audioContext
|
||||||
|
|
||||||
audioContext.onEnded(() => {
|
audioContext.onEnded(() => {
|
||||||
|
console.info('[audio] ended', { playingMessageId: playingMessageIdRef.current })
|
||||||
setPlayingMessageId(null)
|
setPlayingMessageId(null)
|
||||||
})
|
})
|
||||||
audioContext.onStop(() => {
|
audioContext.onStop(() => {
|
||||||
|
console.info('[audio] stopped', { playingMessageId: playingMessageIdRef.current })
|
||||||
setPlayingMessageId(null)
|
setPlayingMessageId(null)
|
||||||
})
|
})
|
||||||
audioContext.onError(() => {
|
audioContext.onError((error) => {
|
||||||
setPlayingMessageId(null)
|
setPlayingMessageId(null)
|
||||||
|
console.error('[audio] error', {
|
||||||
|
playingMessageId: playingMessageIdRef.current,
|
||||||
|
src: audioContext.src,
|
||||||
|
error,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
Taro.showToast({ title: '语音播放失败', icon: 'none' })
|
Taro.showToast({ title: '语音播放失败', icon: 'none' })
|
||||||
})
|
})
|
||||||
|
audioContext.onPlay(() => {
|
||||||
|
console.info('[audio] play event', { playingMessageId: playingMessageIdRef.current, src: audioContext.src })
|
||||||
|
})
|
||||||
|
audioContext.onWaiting(() => {
|
||||||
|
console.info('[audio] waiting', { playingMessageId: playingMessageIdRef.current, src: audioContext.src })
|
||||||
|
})
|
||||||
|
audioContext.onCanplay(() => {
|
||||||
|
console.info('[audio] canplay', { playingMessageId: playingMessageIdRef.current, src: audioContext.src })
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
audioContext.destroy()
|
audioContext.destroy()
|
||||||
@@ -125,6 +294,10 @@ export default function ChatDetail() {
|
|||||||
activeConversationIdRef.current = activeConversationId
|
activeConversationIdRef.current = activeConversationId
|
||||||
}, [activeConversationId])
|
}, [activeConversationId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
playingMessageIdRef.current = playingMessageId
|
||||||
|
}, [playingMessageId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadChatData(activeConversationId)
|
void loadChatData(activeConversationId)
|
||||||
}, [activeConversationId, conversationSource, conversationPeerKind])
|
}, [activeConversationId, conversationSource, conversationPeerKind])
|
||||||
@@ -166,42 +339,85 @@ export default function ChatDetail() {
|
|||||||
latestLoadChatDataRef.current = loadChatData
|
latestLoadChatDataRef.current = loadChatData
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof Taro.getRecorderManager !== 'function') return
|
if (typeof Taro.getRecorderManager !== 'function') {
|
||||||
|
console.warn('[record] RecorderManager unavailable', runtimeInfoRef.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const recorderManager = Taro.getRecorderManager()
|
const recorderManager = Taro.getRecorderManager()
|
||||||
recorderManagerRef.current = recorderManager
|
recorderManagerRef.current = recorderManager
|
||||||
|
|
||||||
|
recorderManager.onStart(() => {
|
||||||
|
console.info('[record] started', {
|
||||||
|
profile: recordProfileRef.current,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
recorderManager.onError((error) => {
|
recorderManager.onError((error) => {
|
||||||
recordingChildIdRef.current = null
|
recordingChildIdRef.current = null
|
||||||
|
recordStartedAtRef.current = 0
|
||||||
setRecording(false)
|
setRecording(false)
|
||||||
setRecordHint('长按开始留言')
|
setRecordHint('长按开始留言')
|
||||||
console.error('[chat-detail] recorder failed:', error)
|
console.error('[record] error', {
|
||||||
|
error,
|
||||||
|
profile: recordProfileRef.current,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
Taro.showToast({ title: '录音失败,请重试', icon: 'none' })
|
Taro.showToast({ title: '录音失败,请重试', icon: 'none' })
|
||||||
})
|
})
|
||||||
|
|
||||||
recorderManager.onStop(async (result) => {
|
recorderManager.onStop(async (result) => {
|
||||||
const currentChildId = recordingChildIdRef.current
|
const currentChildId = recordingChildIdRef.current
|
||||||
|
const startedAt = recordStartedAtRef.current
|
||||||
|
const fallbackDurationMs = startedAt > 0 ? Date.now() - startedAt : 0
|
||||||
recordingChildIdRef.current = null
|
recordingChildIdRef.current = null
|
||||||
|
recordStartedAtRef.current = 0
|
||||||
setRecording(false)
|
setRecording(false)
|
||||||
setRecordHint('长按开始留言')
|
setRecordHint('长按开始留言')
|
||||||
|
|
||||||
|
console.info('[record] stopped', {
|
||||||
|
childId: currentChildId,
|
||||||
|
tempFilePath: result.tempFilePath,
|
||||||
|
duration: result.duration,
|
||||||
|
fileSize: result.fileSize,
|
||||||
|
fallbackDurationMs,
|
||||||
|
profile: recordProfileRef.current,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
|
|
||||||
if (!currentChildId) return
|
if (!currentChildId) return
|
||||||
if (!result.tempFilePath) {
|
if (!result.tempFilePath) {
|
||||||
|
console.warn('[record] missing tempFilePath', { result })
|
||||||
Taro.showToast({ title: '录音文件不存在', icon: 'none' })
|
Taro.showToast({ title: '录音文件不存在', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const durationMs = Math.max(0, Number(result.duration || 0))
|
const durationMs = Math.max(0, Number(result.duration || 0) || fallbackDurationMs)
|
||||||
if (durationMs < 800) {
|
if (durationMs < 800) {
|
||||||
|
console.warn('[record] too short', {
|
||||||
|
durationMs,
|
||||||
|
resultDuration: result.duration,
|
||||||
|
fallbackDurationMs,
|
||||||
|
})
|
||||||
Taro.showToast({ title: '留言太短,请重试', icon: 'none' })
|
Taro.showToast({ title: '留言太短,请重试', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSending(true)
|
setSending(true)
|
||||||
try {
|
try {
|
||||||
|
const profile = recordProfileRef.current
|
||||||
const response = await sendParentVoiceMessage(currentChildId, {
|
const response = await sendParentVoiceMessage(currentChildId, {
|
||||||
filePath: result.tempFilePath,
|
filePath: result.tempFilePath,
|
||||||
durationMs,
|
durationMs,
|
||||||
|
audioFormat: profile.format,
|
||||||
|
mimeType: profile.mimeType,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
|
console.info('[record] uploaded', {
|
||||||
|
childId: currentChildId,
|
||||||
|
conversationId: response.conversation_id,
|
||||||
|
messageId: response.message?.id,
|
||||||
})
|
})
|
||||||
if (response.conversation_id && response.conversation_id !== activeConversationIdRef.current) {
|
if (response.conversation_id && response.conversation_id !== activeConversationIdRef.current) {
|
||||||
setActiveConversationId(response.conversation_id)
|
setActiveConversationId(response.conversation_id)
|
||||||
@@ -232,16 +448,27 @@ export default function ChatDetail() {
|
|||||||
const ensureRecordPermission = async (): Promise<boolean> => {
|
const ensureRecordPermission = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const settings = await Taro.getSetting()
|
const settings = await Taro.getSetting()
|
||||||
if (settings.authSetting['scope.record']) return true
|
const granted = Boolean(settings.authSetting['scope.record'])
|
||||||
|
console.info('[record] permission status', {
|
||||||
|
granted,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
|
if (granted) return true
|
||||||
await Taro.authorize({ scope: 'scope.record' })
|
await Taro.authorize({ scope: 'scope.record' })
|
||||||
|
console.info('[record] permission granted by authorize')
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.warn('[record] permission denied or authorize failed', {
|
||||||
|
error,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
const modal = await Taro.showModal({
|
const modal = await Taro.showModal({
|
||||||
title: '需要录音权限',
|
title: '需要录音权限',
|
||||||
content: '请允许小程序使用麦克风后再留言',
|
content: '请允许小程序使用麦克风后再留言',
|
||||||
confirmText: '去设置',
|
confirmText: '去设置',
|
||||||
})
|
})
|
||||||
if (modal.confirm) {
|
if (modal.confirm) {
|
||||||
|
console.info('[record] openSetting requested')
|
||||||
await Taro.openSetting()
|
await Taro.openSetting()
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -264,22 +491,37 @@ export default function ChatDetail() {
|
|||||||
const hasPermission = await ensureRecordPermission()
|
const hasPermission = await ensureRecordPermission()
|
||||||
if (!hasPermission) return
|
if (!hasPermission) return
|
||||||
|
|
||||||
|
const runtime = readVoiceRuntimeInfo()
|
||||||
|
const profile = getPreferredRecordProfile(runtime)
|
||||||
|
runtimeInfoRef.current = runtime
|
||||||
|
recordProfileRef.current = profile
|
||||||
recordingChildIdRef.current = character.childId
|
recordingChildIdRef.current = character.childId
|
||||||
|
recordStartedAtRef.current = Date.now()
|
||||||
setRecording(true)
|
setRecording(true)
|
||||||
setRecordHint('松开发送留言')
|
setRecordHint('松开发送留言')
|
||||||
|
console.info('[record] start request', {
|
||||||
|
childId: character.childId,
|
||||||
|
profile,
|
||||||
|
runtime,
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
recorderManager.start({
|
recorderManager.start({
|
||||||
duration: 60000,
|
duration: 60000,
|
||||||
format: 'mp3',
|
format: profile.format,
|
||||||
numberOfChannels: 1,
|
numberOfChannels: 1,
|
||||||
sampleRate: 16000,
|
sampleRate: profile.sampleRate,
|
||||||
encodeBitRate: 96000,
|
encodeBitRate: profile.encodeBitRate,
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
recordingChildIdRef.current = null
|
recordingChildIdRef.current = null
|
||||||
|
recordStartedAtRef.current = 0
|
||||||
setRecording(false)
|
setRecording(false)
|
||||||
setRecordHint('长按开始留言')
|
setRecordHint('长按开始留言')
|
||||||
console.error('[chat-detail] recorder start failed:', error)
|
console.error('[record] start failed:', {
|
||||||
|
error,
|
||||||
|
profile,
|
||||||
|
runtime,
|
||||||
|
})
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '无法开始录音,请重试',
|
title: '无法开始录音,请重试',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
@@ -290,9 +532,15 @@ export default function ChatDetail() {
|
|||||||
const handleRecordStop = () => {
|
const handleRecordStop = () => {
|
||||||
if (!recording) return
|
if (!recording) return
|
||||||
const recorderManager = recorderManagerRef.current
|
const recorderManager = recorderManagerRef.current
|
||||||
if (!recorderManager) return
|
if (!recorderManager) {
|
||||||
|
console.warn('[record] stop skipped: recorder unavailable')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setRecordHint('正在处理留言...')
|
setRecordHint('正在处理留言...')
|
||||||
|
console.info('[record] stop request', {
|
||||||
|
elapsedMs: recordStartedAtRef.current > 0 ? Date.now() - recordStartedAtRef.current : 0,
|
||||||
|
})
|
||||||
recorderManager.stop()
|
recorderManager.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,13 +550,29 @@ export default function ChatDetail() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const urlInfo = getAudioUrlDebugInfo(message.mediaUrl)
|
||||||
|
console.info('[audio] play request', {
|
||||||
|
messageId: message.id,
|
||||||
|
conversationId: message.conversationId,
|
||||||
|
contentType: message.contentType,
|
||||||
|
mediaDurationMs: message.mediaDurationMs,
|
||||||
|
mediaMimeType: message.mediaMimeType,
|
||||||
|
url: urlInfo,
|
||||||
|
runtime: runtimeInfoRef.current,
|
||||||
|
})
|
||||||
|
if (urlInfo.isHttp) {
|
||||||
|
console.warn('[audio] http url may fail in experience/release builds', urlInfo)
|
||||||
|
}
|
||||||
|
|
||||||
const audioContext = audioContextRef.current
|
const audioContext = audioContextRef.current
|
||||||
if (!audioContext) {
|
if (!audioContext) {
|
||||||
|
console.warn('[audio] play skipped: audio context unavailable', runtimeInfoRef.current)
|
||||||
Taro.showToast({ title: '播放器未就绪', icon: 'none' })
|
Taro.showToast({ title: '播放器未就绪', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playingMessageId === message.id) {
|
if (playingMessageId === message.id) {
|
||||||
|
console.info('[audio] stop current', { messageId: message.id })
|
||||||
audioContext.stop()
|
audioContext.stop()
|
||||||
setPlayingMessageId(null)
|
setPlayingMessageId(null)
|
||||||
return
|
return
|
||||||
@@ -318,6 +582,11 @@ export default function ChatDetail() {
|
|||||||
audioContext.src = message.mediaUrl
|
audioContext.src = message.mediaUrl
|
||||||
audioContext.autoplay = true
|
audioContext.autoplay = true
|
||||||
audioContext.play()
|
audioContext.play()
|
||||||
|
console.info('[audio] play invoked', {
|
||||||
|
messageId: message.id,
|
||||||
|
srcRaw: message.mediaUrl,
|
||||||
|
src: audioContext.src,
|
||||||
|
})
|
||||||
setPlayingMessageId(message.id)
|
setPlayingMessageId(message.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -690,7 +690,14 @@ function getUploadErrorMessage(data: any): string {
|
|||||||
|
|
||||||
export async function sendParentVoiceMessage(
|
export async function sendParentVoiceMessage(
|
||||||
childId: number,
|
childId: number,
|
||||||
options: { filePath: string; durationMs: number; transcriptText?: string }
|
options: {
|
||||||
|
filePath: string
|
||||||
|
durationMs: number
|
||||||
|
transcriptText?: string
|
||||||
|
audioFormat?: string
|
||||||
|
mimeType?: string
|
||||||
|
runtime?: Record<string, any>
|
||||||
|
}
|
||||||
): Promise<ConversationMessageCreateResponse> {
|
): Promise<ConversationMessageCreateResponse> {
|
||||||
if (!childId) {
|
if (!childId) {
|
||||||
throw new Error('当前会话不支持留言')
|
throw new Error('当前会话不支持留言')
|
||||||
@@ -701,6 +708,16 @@ export async function sendParentVoiceMessage(
|
|||||||
|
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const requestUrl = `${BASE_URL}/banban/children/${childId}/voice-message`
|
const requestUrl = `${BASE_URL}/banban/children/${childId}/voice-message`
|
||||||
|
console.info('[voice-upload] request', {
|
||||||
|
childId,
|
||||||
|
requestUrl,
|
||||||
|
filePath: options.filePath,
|
||||||
|
durationMs: options.durationMs,
|
||||||
|
audioFormat: options.audioFormat || '',
|
||||||
|
mimeType: options.mimeType || '',
|
||||||
|
runtime: options.runtime || {},
|
||||||
|
hasToken: Boolean(token),
|
||||||
|
})
|
||||||
const uploadResponse: any = await new Promise((resolve, reject) => {
|
const uploadResponse: any = await new Promise((resolve, reject) => {
|
||||||
Taro.uploadFile({
|
Taro.uploadFile({
|
||||||
url: requestUrl,
|
url: requestUrl,
|
||||||
@@ -715,9 +732,26 @@ export async function sendParentVoiceMessage(
|
|||||||
duration_ms: `${Math.max(1, Math.round(options.durationMs || 0))}`,
|
duration_ms: `${Math.max(1, Math.round(options.durationMs || 0))}`,
|
||||||
client_msg_id: createClientMessageId(),
|
client_msg_id: createClientMessageId(),
|
||||||
transcript_text: options.transcriptText || '',
|
transcript_text: options.transcriptText || '',
|
||||||
|
audio_format: options.audioFormat || '',
|
||||||
|
mime_type: options.mimeType || '',
|
||||||
|
},
|
||||||
|
success: (response) => {
|
||||||
|
console.info('[voice-upload] response', {
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
errMsg: response.errMsg,
|
||||||
|
dataType: typeof response.data,
|
||||||
|
})
|
||||||
|
resolve(response)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('[voice-upload] fail', {
|
||||||
|
childId,
|
||||||
|
requestUrl,
|
||||||
|
errMsg: error?.errMsg,
|
||||||
|
message: error?.message,
|
||||||
|
})
|
||||||
|
reject(error)
|
||||||
},
|
},
|
||||||
success: resolve,
|
|
||||||
fail: reject,
|
|
||||||
})
|
})
|
||||||
}).catch((error: any) => {
|
}).catch((error: any) => {
|
||||||
const message = String(error?.errMsg || error?.message || 'request:fail')
|
const message = String(error?.errMsg || error?.message || 'request:fail')
|
||||||
@@ -730,6 +764,10 @@ export async function sendParentVoiceMessage(
|
|||||||
|
|
||||||
const data = parseUploadResponseData(uploadResponse.data)
|
const data = parseUploadResponseData(uploadResponse.data)
|
||||||
if (uploadResponse.statusCode >= 400) {
|
if (uploadResponse.statusCode >= 400) {
|
||||||
|
console.error('[voice-upload] error response', {
|
||||||
|
statusCode: uploadResponse.statusCode,
|
||||||
|
data,
|
||||||
|
})
|
||||||
throw new ApiError(uploadResponse.statusCode, getUploadErrorMessage(data))
|
throw new ApiError(uploadResponse.statusCode, getUploadErrorMessage(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -455,15 +455,37 @@ async def create_child_voice_message_for_parent(
|
|||||||
duration_ms: int = Form(..., ge=0),
|
duration_ms: int = Form(..., ge=0),
|
||||||
client_msg_id: str = Form(..., min_length=1, max_length=64),
|
client_msg_id: str = Form(..., min_length=1, max_length=64),
|
||||||
transcript_text: str | None = Form(default=None, max_length=1000),
|
transcript_text: str | None = Form(default=None, max_length=1000),
|
||||||
|
audio_format: str | None = Form(default=None, max_length=16),
|
||||||
|
mime_type: str | None = Form(default=None, max_length=64),
|
||||||
current_user_id: int = Depends(get_current_user_id),
|
current_user_id: int = Depends(get_current_user_id),
|
||||||
) -> ConversationMessageCreateResponse:
|
) -> ConversationMessageCreateResponse:
|
||||||
try:
|
try:
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
|
normalized_format = (audio_format or "").strip().lower().lstrip(".")
|
||||||
|
filename = file.filename or (f"voice.{normalized_format}" if normalized_format else None)
|
||||||
|
content_type = (mime_type or "").strip() or file.content_type
|
||||||
|
logger.info(
|
||||||
|
"parent child voice upload received",
|
||||||
|
extra={
|
||||||
|
"event": "parent_child_voice_upload_receive",
|
||||||
|
"request_id": getattr(request.state, "request_id", None),
|
||||||
|
"user_id": current_user_id,
|
||||||
|
"child_id": child_id,
|
||||||
|
"file_name": file.filename,
|
||||||
|
"effective_file_name": filename,
|
||||||
|
"file_content_type": file.content_type,
|
||||||
|
"client_mime_type": mime_type,
|
||||||
|
"effective_content_type": content_type,
|
||||||
|
"client_audio_format": audio_format,
|
||||||
|
"duration_ms": duration_ms,
|
||||||
|
"size_bytes": len(content),
|
||||||
|
},
|
||||||
|
)
|
||||||
result = await im_service.create_parent_child_voice_message(
|
result = await im_service.create_parent_child_voice_message(
|
||||||
parent_user_id=current_user_id,
|
parent_user_id=current_user_id,
|
||||||
child_id=child_id,
|
child_id=child_id,
|
||||||
filename=file.filename,
|
filename=filename,
|
||||||
content_type=file.content_type,
|
content_type=content_type,
|
||||||
content=content,
|
content=content,
|
||||||
media_duration_ms=duration_ms,
|
media_duration_ms=duration_ms,
|
||||||
media_transcript_text=transcript_text,
|
media_transcript_text=transcript_text,
|
||||||
|
|||||||
Reference in New Issue
Block a user