diff --git a/package.json b/package.json index 3406936..95f057d 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "build:app-ios": "uni build -p app-ios", "build:custom": "uni build -p", "build:h5": "uni build", + "build:h5-dev": "uni build --mode dev", "build": "uni build", "build:h5:ssr": "uni build --ssr", "build:mp-alipay": "uni build -p mp-alipay", diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 8ba8260..56ffafb 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -160,7 +160,7 @@ v-for="(file, fileIdx) in msg.content" :key="fileIdx" style="flex: 0 0 6rem" - class="relative text-xs h-32 w-80 rounded-md overflow-hidden mr-1 c-black" + class="relative text-xs h-32 rounded-md overflow-hidden mr-1 c-black" > <!-- @click="previewVideo(msg.content)"--> <video @@ -184,11 +184,17 @@ </view> </view> <view - v-if="msg.role === 'assistant' && msg.type === 'text'" + v-if=" + msg.role === 'assistant' && msg.type === 'text' && messages.length - 1 === idx + " class="absolute bottom--3.5 flex space-x-3 ml-1" > <image src="/static/aichat/copy.png" class="w-4 h-4" @click="copyText(msg)" /> - <image src="/static/aichat/resect.png" class="w-4.3 h-4" @click="refreshText()" /> + <image + src="/static/aichat/resect.png" + class="w-4.3 h-4" + @click="refreshText(msg)" + /> </view> </view> <image @@ -332,6 +338,7 @@ @focus="onFocus" @confirm="sendText" placeholder="想对我说点什么~" + maxlength="5000" class="flex-1 h-10 px-3 border border-gray-100 bg-[#f9f9f9] rounded-1 focus:outline-none" /> <!-- 将keyup替换为confirm --> @@ -349,7 +356,6 @@ v-if="sendTextLoading && inputText.length <= 0" src="/static/aichat/enter-no.png" class="w-7 h-7" - @click="sendText()" :disabled="loading" :class="[knowledgeOpen ? 'ml-2' : 'ml-0']" /> @@ -477,6 +483,7 @@ import { ref, reactive, nextTick, watchEffect, watch } from 'vue' import dayjs from 'dayjs' import { useUserStore } from '@/store' +// import store from '@/store' import { getEnvBaseUrl } from '@/utils' import guid from '@/utils/guid.js' import type { IGptRequestBody } from '@/service/index/foo' @@ -489,15 +496,16 @@ import { officeFileTypeList as fileType, videoFileType as videoType, picFileType as picType, + isJsonObject, } from './utils/index' import 'dayjs/locale/zh-cn' import { showToastErr, showToastOk, time_format3 } from '@/utils/tools' import { uploadFileChunk } from './utils/api.js' // import { TOKEN, AVATAR } from './utils/test' import { deepClone } from 'wot-design-uni/components/common/util' -import { log } from 'console' dayjs.locale('zh-cn') +const store = useUserStore() interface UploadFile { id: string @@ -605,7 +613,7 @@ async function createChatSession() { gptModel: chatMode.value, }, header: { - Authorization: token.value, + // Authorization: token.value, }, }) // 如果后台返回新的会话信息,可以在这里处理,比如拿到 listUuid 等 @@ -627,9 +635,9 @@ const state = reactive({ }) const scrollTop = ref(0) async function fetchHistoryList() { - // if(state.page*state.pageSize>state.total && state.total!==null){ - // return - // } + if (state.page >= state.total / state.pageSize + 1 && state.total !== null) { + return + } if (state.loading) { return } @@ -643,12 +651,18 @@ async function fetchHistoryList() { pageSize: state.pageSize, }, header: { - Authorization: token.value, + // Authorization: token.value, }, }) if (resp.data && resp.data.data) { - rawList.value = rawList.value.concat(resp.data.data.data) - state.total = resp.data.data.count //Math.ceil(resp.data.count/state.page) + if (state.total === null) { + rawList.value = resp.data.data.data + state.total = resp.data.data.count + } else { + rawList.value = rawList.value.concat(resp.data.data.data) + state.total = resp.data.data.count //Math.ceil(resp.data.count/state.page) + } + // scrollTop.value+=60; } } catch (err) { @@ -662,7 +676,10 @@ const scrolltolowerLoadData = (e) => { } watch( () => state.page, - async () => { + async (newval) => { + if (newval <= 0) { + return + } await fetchHistoryList() }, { deep: true }, @@ -670,9 +687,12 @@ watch( async function openPopup() { state.page++ showPopup.value = true + rawList.value = [] } function closePopup() { showPopup.value = false + state.page = 0 + state.total = null } function toggleFullscreen() { fullscreen.value = !fullscreen.value @@ -689,12 +709,12 @@ async function fetchHistoryDiets(value) { gptModel: chatMode.value, }, header: { - Authorization: token.value, + // Authorization: token.value, }, }) if (resp && resp.data && resp.data.data) { - const rawList = resp.data.data.detail // 假设后端直接返回消息数组 - listUuid.value = resp.data.data.listUuid + const rawList = resp?.data?.data?.detail // 假设后端直接返回消息数组 + listUuid.value = resp?.data?.data?.listUuid // const newMessages = parseBackendMessages(JSON.parse(rawList)) // 用解析后的消息替换当前消息列表 messages.splice(0, messages.length, ...JSON.parse(rawList)) @@ -971,18 +991,30 @@ onMounted(async () => { try { const init = async () => { const wv = plus.webview.currentWebview() // 获取当前页面所属的 Webview 对象。 - token.value = wv.token || uni.getStorageSync('token') || import.meta.env.VITE_DEV_TOKEN + token.value = + wv.token || + uni.getStorageSync('token') || + store.userInfo.token || + import.meta.env.VITE_DEV_TOKEN userInfo.value = JSON.parse(wv.userInfo) || {} refreshToken.value = wv.refreshToken || uni.getStorageSync('refreshToken') statusBarHeight.value = wv.statusBarHeight || uni.getSystemInfoSync().statusBarHeight userAvatar.value = userInfo.value.Avatar mask.value = userInfo.value.ID + store.setUserInfo({ + token: token.value, + avatar: userInfo.value.Avatar, + refreshToken: refreshToken.value, + statusBarHeight: statusBarHeight.value, + }) await createChatSession() } init() } catch (e) { console.error('onMounted e: ', e) } finally { + token.value = store.userInfo.token + await createChatSession() } }) @@ -1411,12 +1443,60 @@ const onPickImage = () => { }, }) } +// 调用原生 Android API 拍摄视频 + +const onPickVideo3 = () => { + var cmr = plus.camera.getCamera() + + try { + cmr.startVideoCapture( + () => { + alert('ok') + }, + () => { + alert('err') + }, + {}, + ) + } catch (e) { + } finally { + cmr.stopVideoCapture() + } +} -// 视频 const onPickVideo = () => { uni.chooseVideo({ - sourceType: ['album', 'camera'], + // sourceType: ['album', 'camera'], + sourceType: ['album'], + maxDuration: 60, compressed: true, + camera: 'back', + albumMode: 'custom', + // extension: uploadConfig.video.supportType, + success: (res: any) => { + console.log(res) + + const tempFile = res.tempFile + tempFile.path = res.tempFilePath + + // 开始上传 + addUploadQueue([tempFile], uploadFileTypeEm.video) + }, + fail: (err) => { + uni.showToast({ + title: '选取视频失败', + icon: 'none', + }) + }, + }) +} +// 视频 +const onPickVideo2 = () => { + uni.chooseVideo({ + sourceType: ['album', 'camera'], + maxDuration: 60, + compressed: true, + camera: 'back', // extension: uploadConfig.video.supportType, success: (res: any) => { console.log(res) @@ -1561,7 +1641,6 @@ const stopMsg = () => { stopStreamMsg = true } async function sendText() { - console.log('uploadList: ', uploadList) if (uploadList.length > 0) { const isUpLoading = uploadList.some((file) => { // return file.status==="error" || file.status==="pending" @@ -1578,18 +1657,16 @@ async function sendText() { } } const msg = inputText.value.trim() - if (!msg && !refreshSend.value) { + if (!msg) { return showToastErr('不可以发送空消息!') } // if (uploadList.length > 0) { // return showToastErr('请等待文件上传完成!') // } if (!sendTextLoading.value) { - sendTextLoading.value = true return showToastErr('正在接收消息请稍后') } - // 开启加载状态 - sendTextLoading.value = false + // 文本消息 // 先判断是否上传文件,若有文件在判断文件类型,视频+图片不能与文档类文件同时上传 @@ -1741,16 +1818,16 @@ async function sendText() { } } } else { - if (!refreshSend.value) { - //不重发时触发 - addMessage({ - role: 'user', - type: 'text', - content: msg, - timestamp: new Date(), - mask: 'new', - }) - } + // if (!refreshSend.value) { + //不重发时触发 + addMessage({ + role: 'user', + type: 'text', + content: msg, + timestamp: new Date(), + mask: 'new', + }) + // } // 纯文本时发送 chatMode.value = 'tongyi-app' //'tongyi-app'; qwen-long @@ -1782,7 +1859,6 @@ async function sendText() { // 第一次发送纯文本消息,第二次发送图片+视频,第三次发送文档,此时因为历史消息都要一起发送给后端, // 所以要想办法在遇到这种情况时,截断历史记录,主动为用户建立一个新的回话,但是不需要清空历史记录 - console.log('message: ', messages) uploadList.splice(0, uploadList.length) // 清空上传的文件 const list = formatData(messages) @@ -1790,9 +1866,38 @@ async function sendText() { body.detail = JSON.stringify(messages) aiMsg.content = '' addMessage(aiMsg) + send(body) // return // return // 没有上传文件,仅文字消息 +} +const spliceMsg = (list, model) => { + let file = false + let image = false + let video = false + const length = list.length + console.log(list) + + for (let i = length - 1; i >= 0; i--) { + const item = list[i] + if (model === 'tongyi-long' && item.type === 'image' && item.type === 'video') { + const index = length - i - 1 + return list.splice(i - 1) + } else if (model !== 'tongyi-long' && item.type === 'file') { + const index = i + return list.splice(index + 1) + } + } + return list +} +const send = async (body) => { + // refreshSend.value = true; // 正在重新发送 + // 开启加载状态 + sendTextLoading.value = false // 接收消息期间不可再次发送 + const [aiMsg] = messages.slice(-1) + const recordList = messages.slice(0, messages.length - 1) + body.detail = JSON.stringify(recordList) + // body.messages = spliceMsg(body.messages, chatMode.value) try { // aiMsg.content = '' // 发送问题到后端 @@ -1801,10 +1906,14 @@ async function sendText() { const signal = controller.signal const resp = await fetch(baseUrl + '/chat/app/completion', { method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: token.value }, + headers: { + 'Content-Type': 'application/json', + Authorization: token.value, + }, body: JSON.stringify(body), signal: signal, }) + console.log(resp) const reader = resp.body!.getReader() const decoder = new TextDecoder() @@ -1839,13 +1948,17 @@ async function sendText() { const json = JSON.parse(chunk) const delta = json.choices?.[0]?.delta?.content if (delta) { - msgLoading.value = false aiMsg.content += delta + // msgLoading.value = false //每次更新messages消息,实现流式输出 messages[messages.length - 1] = { ...aiMsg } scrollToBottom() } - } catch {} + } catch (e) { + console.log(e) + } finally { + console.log('over') + } } //更新上下文消息 @@ -1871,6 +1984,13 @@ async function sendText() { console.log('chunk------------------: ') } } + if (isJsonObject(buffer)) { + const response = JSON.parse(buffer) + if (response.code === 401) { + showToastErr('请重新登录') + } + } + scrollToBottom() } catch (err) { // aiMsg.content = '请重新发送' @@ -1880,7 +2000,7 @@ async function sendText() { } finally { sendTextLoading.value = true showActions.value = false - refreshSend.value = false // 重发已经结束 关闭重发 + // refreshSend.value = false // 重发已经结束 关闭重发 msgLoading.value = false } } @@ -1899,7 +2019,16 @@ function copyText(msg: IMessage) { } } -function refreshText() { +const msgType = (msg) => { + return ( + msg && + msg.role === 'user' && + (msg.type === 'text' || msg.type === 'image' || msg.type === 'video') + ) +} +function refreshText(msg) { + console.log('refresh msg', msg) + if (!sendTextLoading.value) { // 正在接收消息,不可以重发 return @@ -1910,98 +2039,57 @@ function refreshText() { // 1. 找到最后两条用户消息(用于处理图文混合场景) const userMessages = messages.filter((msg) => msg.role === 'user') // const lastTwoUserMsgs = userMessages.slice(-2) - const [msg1, msg2] = deepClone(userMessages.slice(-2)) - // let text=lastTwoUserMsgs[0] // lastTwoUserMsgs.every((msg)=>msg.type==="text" || msg.type==="image" || msg.type==="video" || msg.type==="file") - // let file=lastTwoUserMsgs[1] // lastTwoUserMsgs.every((msg)=>msg.type==="text" || msg.type==="image" || msg.type==="video" || msg.type==="file") + // const [msg1, msg2, msg3] = deepClone(userMessages.slice(-3)) + const newMsgArr = deepClone(userMessages.slice(-3)) // 2. 提取文本内容和文件列表 - let refreshText = null + let text = null const refreshFiles: UploadFile[] = [] - if (msg1 && msg1.type === 'text' && msg2 && msg2.type !== 'text') { - msg1.mask = 'new' - msg2.mask = 'new' - refreshFiles.push(msg1) - refreshFiles.push(msg2) - } else if (msg1.type === 'text' && msg1.role === 'user' && !msg2) { - msg1.mask = 'new' - refreshFiles.push(msg1) - } else if (msg2.type === 'text' && msg2.role === 'user' && !msg1) { - msg2.mask = 'new' - refreshFiles.push(msg2) - } else { - msg2.mask = 'new' - refreshFiles.push(msg2) + for (let i = newMsgArr.length - 1; i >= 0; i--) { + const msg = newMsgArr[i] + if (msg.type === 'text') { + refreshFiles.unshift(msg) + break + } else { + refreshFiles.unshift(msg) + } } - // lastTwoUserMsgs.forEach((msg,i) => { - // console.log('msg: ',msg); - // if (msg.type === 'text' && msg.role==="user") { - // refreshText = msg.content // 总是取最新的文本 - // refreshFiles.push({ - // content:msg.content, - // type:"text", - // role:"user", - // timestamp:new Date(), - // mask:"new" - // }) - // } else if(msg.type==="video"){ - // msg.mask="new" - // refreshFiles.push(msg) - // // msg.content.forEach((file : any) => { - // // console.log('lastTwoUserMsgs file: ',file); - - // // refreshFiles.push({ - // // id: guid.getGuid(), - // // url: file.video_url.url, - // // status: 'success', - // // name: file.name || '未命名文件', - // // size: file.size || 0, - // // uploadFileType: file.uploadFileType || detectFileType(file.video_url.url), - // // }) - // // }) - // }else if(msg.type==="image"){ - // msg.mask="new" - // refreshFiles.push(msg) - // // msg.content.forEach((file : any) => { - // // console.log('lastTwoUserMsgs file: ',file); - // // refreshFiles.push({ - // // id: guid.getGuid(), - // // url: file.image_url.url, - // // status: 'success', - // // name: file.name || '未命名文件', - // // size: file.size || 0, - // // uploadFileType: file.uploadFileType || detectFileType(file.image_url.url), - // // }) - // // }) - // }else{ - // msg.mask="new" - // refreshFiles.push(msg) - // // msg.content.forEach((file : any) => { - // // console.log('lastTwoUserMsgs file: ',file); - // // refreshFiles.push({ - // // id: guid.getGuid(), - // // url: file.content, - // // status: 'success', - // // name: file.name || '未命名文件', - // // size: file.size || 0, - // // uploadFileType: file.uploadFileType || detectFileType(file.content), - // // }) - // // }) - // } - // }) - // 3. 更新输入框和上传列表 - // inputText.value = refreshText + // inputText.value = text // uploadList.splice(0, uploadList.length, ...refreshFiles) refreshFiles.forEach((ele) => { messages.push(ele) }) - refreshSend.value = true - // inputText.value = refreshText + + // return + + // refreshSend.value = true + // inputText.value = text // 4. 自动触发发送(模拟用户点击发送按钮) - setTimeout(() => { - sendText() - }, 100) + // setTimeout(() => { + // sendText() + // }, 100) + let list = formatData(messages) + const aiMsg = { + role: 'assistant', + type: 'text', + content: '', + timestamp: new Date(), + } + messages.push(aiMsg) + const body = { + model: chatMode.value, // 模型选择 + max_tokens: 1000, + top_p: 1, + presence_penalty: 0, + frequency_penalty: 0, + messages: list, // text ? [aiMsg] : historyUserMsgs, + stream: true, + listUuid: listUuid.value, + // listUuid:"eff18a10-1719-4528-ad63-ee5c01d0a412" + } + send(body) } // 文件类型检测函数(根据URL后缀) diff --git a/src/pages/index/utils/data.js b/src/pages/index/utils/data.js index ce35b5c..c53b5cd 100644 --- a/src/pages/index/utils/data.js +++ b/src/pages/index/utils/data.js @@ -202,4 +202,6 @@ async function sendText1(msgData = '') { loading.value = false showActions.value = false } -} \ No newline at end of file +} + +export const TOKEN="79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941ca1430937103230a1e32a1715f569f3efdbe6f8cb8b7b8642bd679668081b9b08f693d1b5be6002d936ec51e1e3e0c4927de9e32ac99a109b326e5d2bda27ec87624bb416ec70d2a95a2e190feeba9f0d6bae8571b3dfe89c824712344759a8f2bff9d70747c52525cf6a5614f9c770bca461a9b9c247b6dca97bcf83bbaf99bb726752c4fe1e9a4aa7de5c4cf3e88a3e480801280d45cdc124f9d8221105d852945dc6ce10bc1647e4f09dff4d52ffdfcd57b2349fd3262098015f94b8786aabc0d8a8098a126bf2449839db7ded893783707a3f776e4ff20f9e79ce24ba97e5f82085d12a8e518fe6dedcd453a773bb4cb26657088a4b3cd06b62cd9f9738196" \ No newline at end of file diff --git a/src/pages/index/utils/index.ts b/src/pages/index/utils/index.ts index 38b2c6e..4ea81ab 100644 --- a/src/pages/index/utils/index.ts +++ b/src/pages/index/utils/index.ts @@ -5,6 +5,10 @@ export const fileSuffix = (str) => { let reg = /\.\w*$/ return str.match(reg)[0] } +export const isJsonObject = (json) => { + const str = json.trim() + return str.startsWith('{') && str.endsWith('}') +} export const officeFileTypeList = ['.docx', '.doc', '.xls', '.xlsx', '.pdf', '.txt'] export const videoFileType = ['.mp4', '.mov', '.wmv'] export const picFileType = ['.jpg', '.png', '.jpeg'] @@ -112,7 +116,7 @@ export function formatData(list) { result.push({ role: 'user', content: content, - type: 'image', + type: item.type, mask: item.mask, }) } else if (item.type === 'file') { @@ -171,15 +175,3 @@ export async function readFile(file, chunkSize = 10 * 1024 * 1024) { const buffer = await blob.blob() return sliceFile(buffer, chunkSize) } - -function uploadChunkFile({ chunk, fileName }, index, total, fileId) { - const formData = new FormData() - formData.append('Chunk', chunk) - formData.append('ChunkFileName', `${fileName}_${index}`) - formData.append('total', total) - formData.append('UseType', 100) - formData.append('FileName', fileName) - formData.append('Source', 'aiChat') - formData.append('UseType', 100) - return -}