小程序 【小橙有门】- 有门AI 模块
对话时,消息是通过SSE的方式交互,前端通过 requestTask.onChunkReceived 处理数据包。我在体验版中与正式环境中的都使用生产环境的服务API时(统一的后端环境)时,体验版能够正常显示打字机效果,查看调试数据,存在很多的数据包。假设总共有100个字符返回,有50个包的数据被处理,每个数据包有一组~2组数据(onChunkReceived)。但是在生产环境中查看,只有三个数据包,每个包里面有30多组数据。所以没有打字机效果了,显示时是一大段一大段的出现。
sendMessage() {
const { isLoading, inputText, isDeepseek, isOnlineSearch, chatId } = this.data
if (isLoading) {
return
}
if (getApp().isNotLoginAndToLoginPage()) {
return
}
const prompt = inputText.trim()
if (!prompt) return
this.setData({
isLoading: true,
messages: [...this.data.messages, { role: 'user', content: prompt }],
inputText: ''
})
requestTask = doorAiApi.streamChat(prompt, isDeepseek, isOnlineSearch, chatId)
loadingChunkCount = 0
requestTask.onChunkReceived(res => {
try {
this.processChunkData(res.data)
} catch (e) {
console.error(e)
LogUtil.realtimeLog('ai_process_chunk_error', e)
}
})
},
processChunkData(arrayBuffer) {
console.log('arrayBuffer', arrayBuffer)
const uint8Array = new Uint8Array(arrayBuffer)
const decoder = new TextDecoder()
let chunkStr = decoder.decode(uint8Array)
// Replace non line breaking spaces
chunkStr = chunkStr.replace(/\u00A0/g, ' ')
// Split SSE packets
const packets = chunkStr.split('\n\n').filter(str => str !== '')
console.log('packets', packets)
for (const packet of packets) {
if (packet.startsWith('data:')) {
// Discard incomplete messages
if (!SSE_PATTERN.test(packet)) {
continue
}
let jsonStr = packet.replace('data:', '')
if (jsonStr == '') {
continue
}
const response = JSON.parse(jsonStr)
if (response.success === false) {
LogUtil.realtimeLog('ai_network_error', response)
ToastUtil.show('服务器错误', 'error')
this.abortAiChat()
console.error(response.message)
break
}
const thinkText = response.data.think || ''
const content = response.data.answer || ''
if (content === '[[Done]]') {
console.log('steam complete')
const chatId = response.data.chatId || undefined
this.setData({
chatId
})
this.requestDone()
break
}
const streamingThinkText = this.data.streamingThinkText + sanitizeText(thinkText)
const streamingText = this.data.streamingText + sanitizeText(content)
this.setData({
streamingThinkText,
streamingText,
streamingTextNodes: towxml(streamingText, 'markdown', {})
})
// Control Content Scroll Down
if (loadingChunkCount < 100) {
wx.nextTick(() => {
this.scrollToBottom()
})
loadingChunkCount += 1
}
}
}
},
onInput(e) {
this.setData({ inputText: e.detail.value })
},
abortAiChat() {
if (requestTask) {
console.log('request abort')
requestTask.abort()
this.requestDone()
}
},
requestDone() {
this.setData({
isLoading: false,
messages: [
...this.data.messages,
{
role: 'assistant',
thinkText: this.data.streamingThinkText,
content: this.data.streamingText,
contentNodes: this.data.streamingTextNodes
}
],
streamingThinkText: '',
streamingText: '',
streamingTextNodes: {}
})
requestTask = null
},