Browse Source

feat(msg): 优化消息加载和总结功能

- 添加消息加载状态管理
- 优化总结功能,支持页面和文件总结
- 实现表单数据抽取功能
- 优化消息发送和接收逻辑
- 调整消息展示和滚动处理
chd 4 months ago
parent
commit
7b14911a6c

+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "crypto-js": "^4.2.0",
     "dompurify": "^3.2.5",
     "element-plus": "^2.9.1",
+    "eventsource-polyfill": "^0.9.6",
     "highlight.js": "^11.11.1",
     "jsencrypt": "^3.3.2",
     "lodash": "^4.17.21",

+ 23 - 1
src/api/modal.js

@@ -3,11 +3,33 @@ import request from '@/utils/request'
 export function askQues(data) {
     return request({
         url: '/ai/model/question',
-        method: 'get',
+        method: 'post',
         data: data
     })
+    
 }
 
+
+// export async function  askQues(data) {
+//     const { token } = await new Promise((resolve) => {
+//         chrome.storage.local.get(['token'], (result) => {
+//             resolve(result)
+//         })
+//     })
+
+//     // 当token不存在时,执行退出登录
+//     // if (!token && !config.url.includes('/login')) {
+//     //   return Promise.reject('请先登录')
+//     // }
+//     return fetch(import.meta.env.VITE_APP_BASE_API + '/ai/model/question', {
+//         method: 'POST',
+//         headers: {
+//             ['Authorization'] : 'Bearer ' + token
+//         },
+//         body: JSON.stringify(data),
+//     });
+// }
+
 export function getFormKey(data) {
     return request({
         url: '/ai/model/callAlgorithm',

+ 242 - 87
src/entrypoints/sidepanel/Chat.vue

@@ -1,18 +1,18 @@
 <!-- Chat.vue -->
 <template>
   <div class="chat-container">
-    <div v-if="!messages.length" class="message-list">
+    <div v-if="!messages.length && !msgLoading" class="message-list">
       <pageMask />
     </div>
     <!-- 消息列表 -->
-    <div class="message-list" v-else>
-      <el-scrollbar ref="scrollbar" @scroll="handleScroll">
+    <div class="message-list" v-else >
+      <el-scrollbar ref="scrollbar" @scroll="handleScroll" v-loading="msgLoading">
         <!-- 加载更多指示器 -->
         <div v-if="isLoadingMore" class="loading-more-indicator">
           <div class="loading-spinner"></div>
           <span>加载更多消息...</span>
         </div>
-        <div class="messages" ref="messagesContainer">
+        <div class="messages" ref="messagesContainer" >
           <div v-for="(message, index) in messages" :key="index"
             :class="['message-item', message.role === 'user'  ? 'self' : 'other']">
             <el-avatar :size="32" :src="message.role === 'user' ? userAvatar : avatar" />
@@ -75,10 +75,10 @@
 
           <div class="card-btn">
             <el-tooltip  content="总结当前页面" placement="top">
-              <el-button v-if="type !== FunctionList.Intelligent_Form_filling"  round @click="handleSummary">总结</el-button>
+              <el-button :disabled="disabledBtn" v-if="type !== FunctionList.Intelligent_Form_filling"  round @click="handleSummary">总结</el-button>
             </el-tooltip>
              <el-tooltip   content="抽取表单信息" placement="top">
-              <el-button v-if="type === FunctionList.Intelligent_Form_filling && pageInfoList.length" round @click="handleSummary">抽取</el-button>
+              <el-button :disabled="disabledBtn" v-if="type === FunctionList.Intelligent_Form_filling && pageInfoList.length" round @click="handleSummaryFile">抽取</el-button>
             </el-tooltip>
           </div>
         </div>
@@ -139,7 +139,9 @@ import { useMsgStore } from '@/store/modules/msg.ts'
 import { useUserStore } from '@/store/modules/user'
 import { putChat, uploadFile } from "@/api/index.js";
 import {askQues,getFormKey} from '@/api/modal.js'
-import {debounce} from 'lodash'
+import { debounce } from 'lodash'
+import request from '@/utils/request'
+import  EventSourcePolyfill  from 'eventsource-polyfill';
 const userStore = useUserStore()
 import { getChatDetail } from '@/api/index.js'
 // 在其他状态变量附近添加
@@ -155,9 +157,8 @@ const tareRef = useTemplateRef('textareaRef')
 const drawerRef = useTemplateRef('historyComponentRef')
 // 获取父组件提供的 Hook 实例
 const msgStore = useMsgStore()
-const { pageInfoList, messages, msgUuid, AIModel,page,hasNext } = storeToRefs(useMsgStore())
+const { pageInfoList, messages, msgUuid, AIModel,page,hasNext,msgLoading } = storeToRefs(useMsgStore())
 const formInfo = ref('')
-const uploadLoading = ref(false)
 const {
   taklToHtml,
   sendLoading,
@@ -169,6 +170,7 @@ const {
 } = useMsg(scrollbar)
 const inputMessage = ref('')
 const pageInfo = ref('')
+const summaryHtml = ref(false)
 function handleStopAsk() {
   if (type.value === FunctionList.Intelligent_Form_filling) {
     inputMessage.value = ''
@@ -246,19 +248,73 @@ async function addMessage(msg, raw, type) {
   }
   if (!msg) return
   messages.value.push(newMessage)
-  console.log(scrollbar.value);
-  scrollbar.value?.scrollTo(Number.MAX_VALUE)
   nextTick(() => {
-    
+    if (scrollbar.value && scrollbar.value.wrapRef) {
+      scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+    }
   })
   return newMessage
 }
 
 const handleSummary = async () => {
   if(sendLoading.value) return
-  handleAsk('请帮我总结当前文件')
+  handleAsk(`请帮我总结当前${summaryHtml.value ? '页面' : '文件'}`)
+  summaryHtml.value = false
+}
+const handleSummaryFile = async () => {
+  if (sendLoading.value) return
+ try {
+   sendLoading.value = true
+   const  userMsg = await addMessage(JSON.stringify([...pageInfoList.value, { type: 'text', value: '抽取表单' }]), '抽取表单', 'document')
+    userMsg.fileId = pageInfoList.value[0].fileId
+    userMsg.redisKey = pageInfoList.value[0].redisKey
+    await putChat(userMsg)
+    const msg = reactive({
+    type: '',
+    rawContent: '',
+    senderId: -1,
+    receiverId: userStore.userInfo.id, //大模型
+    content: '',
+    sortKey: moment().valueOf(),
+    role: 'system',
+    conversationId: msgUuid.value,
+    })
+  messages.value.push(msg)
+     isShowPage.value = false
+  pageInfoList.value = []
+    const resForm = await askQues({
+    conversationId: msgUuid.value,
+    modelName: '通义千问-Max',
+    question: buildObjPrompt(xlsxData.value,formInfo.value),
+    id: '699637194561691650',
+    // redisKey:msg.redisKey
+    })
+  return console.log(resForm);
+  
+  const formResult = resForm.data[0].content
+    let form = null
+    if (formResult.includes('json')) form = JSON.parse(formResult.split('json')[1].split('```')[0])
+          else form = JSON.parse(formResult)
+  msg.type = 'form'
+  // 保存原始内容
+  msg.rawContent = formResult
+  putChat({
+    ...msg
+  })
+  sendLoading.value = false
+  handleInput(xlsxData.value, form)
+    nextTick(() => {
+    if (scrollbar.value && scrollbar.value.wrapRef) {
+      scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+    }
+  })
+ } catch (error) {
+  
+ } finally {
+   sendLoading.value = false
+  type.value = FunctionList.File_Operation
+ }
 }
-
 async function handelIntelligentFillingClick() {
   // isShowPage.value = true
   // taklToHtml.value = true
@@ -307,6 +363,7 @@ async function readClick() {
     pageInfoList.value = []
     type.value = FunctionList.File_Operation
   }
+  summaryHtml.value = true
   isShowPage.value = true
   taklToHtml.value = true
   if (pageInfoList.value.length >= Number(import.meta.env.VITE_MAX_FILE_NUMBER)) {
@@ -344,21 +401,22 @@ function addNewDialogue() {
   hasNext.value = false
   chrome.storage.local.set({ msgUuid: msgUuid.value })
 }
-
+const disabledBtn = computed(() => {
+ return  !!(pageInfoList.value.find(_ => _.loading ))
+})
 async function handleAsk(value) {
   const str = value ?? inputMessage.value.trim()
+  inputMessage.value = ''
   if (type.value === FunctionList.Intelligent_Form_filling) {
      await addMessage(str)
      const res = await fetchRes(str)
     if (res.status === 'ok') {
       formInfo.value = res.data
-      console.log(res, 55558)
     } else {
       type.value = FunctionList.File_Operation
     }
     return 
   }
-  inputMessage.value = ''
   if (sendLoading.value) return
   let msg = null
   if (pageInfoList.value.length) {
@@ -368,13 +426,7 @@ async function handleAsk(value) {
   } else msg = await addMessage(str)
   await putChat(msg)
   sendLoading.value = true
-  // messages.value.push(obj)
-  nextTick(() => {
-  if (scrollbar.value && scrollbar.value.wrapRef) {
-    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
-  }
-})
-   const obj = reactive({
+  const obj = reactive({
     type: type || '',
     rawContent: '',
     senderId: -1,
@@ -383,63 +435,158 @@ async function handleAsk(value) {
     sortKey: moment().valueOf(),
     role: 'system',
     conversationId: msgUuid.value,
-    })
+  })
   messages.value.push(obj)
-  const res = await askQues({
+  nextTick(() => {
+    if (scrollbar.value && scrollbar.value.wrapRef) {
+      scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+    }
+  })
+  request.post('/ai/model/question',{
     conversationId: msgUuid.value,
     modelName: '通义千问-Max',
     question: msg.rawContent,
     id: '699637194561691650',
-    redisKey:msg.redisKey
+    // redisKey:msg.redisKey
+    }, {
+    onDownloadProgress: progressEvent => {
+      console.log('progressEvent', progressEvent)
+      let { responseText } = progressEvent.event.target
+      const newData = responseText.slice(buffer.length)
+      buffer += newData
+      // const events = buffer.split('\n\n')
+      const events = buffer.split('\n\n').map(item => {
+        const data = item.replace(/^data: /, '').trim()
+        // return data
+        try {
+          return JSON.parse(data)
+        } catch (error) {
+          return ''
+        }
+      })
+      debugger
+      buffer = events.pop() // 剩余部分保留到下一次
+      // events.forEach(event => {
+      //   if (event.trim()) {
+      //     const data = event.replace(/^data: /, '').trim();
+      //     const infoData = JSON.parse(data);
+      //     console.log('Received event:', infoData.message);
+      //   }
+      // });
+      console.log('Received data:', events, 'aaa', buffer)
+    }
   })
-  const result = res.data[res.data.length - 1].content
-  isShowPage.value = false
-  pageInfoList.value = []
-  // 保存原始内容
-  obj.rawContent = result
-  
-  // 实现打字机效果
-  const formattedText = formatMessage(result)
-  const typewriterEffect = async (text) => {
-    let currentIndex = 0
-    const totalLength = text.length
-    const typingSpeed = 10 // 打字速度,可以调整
+  // 使用 WebSocket 替代 EventSource
+  // try {
+  //   // 创建 WebSocket 连接
+    //  await askQues({
+    // conversationId: msgUuid.value,
+    // modelName: '通义千问-Max',
+    // question: msg.rawContent,
+    // id: '699637194561691650',
+    // // redisKey:msg.redisKey
+    // })
+  //   const wsUrl = `ws:${wsBaseUrl}/question/${msgUuid.value}`;
+  //   const socket = new WebSocket(wsUrl);
+  //   // 连接建立时发送消息
+  //   socket.onopen = () => {
+  //     console.log('WebSocket 连接已建立');
+  //     // 发送请求数据
+  //     // socket.send(JSON.stringify({
+  //     //   conversationId: msgUuid.value,
+  //     //   modelName: '通义千问-Max',
+  //     //   question: msg.rawContent,
+  //     //   id: '699637194561691650',
+  //     //   redisKey: msg.redisKey,
+  //     //   token: userStore.token
+  //     // }));
+  //   };
     
-    // 清除之前的内容
-    obj.content = ''
+  //   // 接收消息
+  //   socket.onmessage = (event) => {
+  //     try {
+  //       const data = JSON.parse(event.data);
+  //       console.log('收到数据:', data);
+  //       return
+  //       // 追加内容
+  //       if (data.content) {
+  //         obj.content += data.content;
+  //         obj.rawContent += data.content;
+          
+  //         // 滚动到底部
+  //         nextTick(() => {
+  //           if (scrollbar.value && scrollbar.value.wrapRef) {
+  //             scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight);
+  //           }
+  //         });
+  //       }
+        
+  //       // 如果是最后一条消息,关闭连接
+  //       if (data.done) {
+  //         socket.close();
+  //         sendLoading.value = false;
+          
+  //         // 保存到数据库
+  //         putChat({
+  //           ...obj,
+  //           content: obj.content
+  //         });
+          
+  //         isShowPage.value = false;
+  //         pageInfoList.value = [];
+  //       }
+  //     } catch (error) {
+  //       console.error('解析消息出错:', error);
+  //     }
+  //   };
     
-    // 逐字添加内容
-    const typing = setInterval(() => {
-      if (currentIndex < totalLength) {
-        obj.content += text[currentIndex]
-        currentIndex++
-        // 滚动到底部,保持视图跟随
-        nextTick(() => {
-  if (scrollbar.value && scrollbar.value.wrapRef) {
-    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
-  }
-})
-      } else {
-        clearInterval(typing)
-        sendLoading.value = false
-        // 打字完成后再次滚动到底部
-        nextTick(() => {
-  if (scrollbar.value && scrollbar.value.wrapRef) {
-    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
-  }
-})
-      }
-    }, typingSpeed)
-  }
-  
-  // 开始打字机效果
-  await typewriterEffect(formattedText)
+  //   // 处理错误
+  //   socket.onerror = (error) => {
+  //     console.error('WebSocket 错误:', error);
+  //     socket.close();
+  //     sendLoading.value = false;
+      
+  //     // 如果没有收到任何内容,显示错误消息
+  //     if (!obj.content) {
+  //       obj.content = '连接出错,请重试';
+  //       // 保存到数据库
+  //       putChat({
+  //         ...obj,
+  //         content: obj.content
+  //       });
+  //     }
+  //   };
+    
+  //   // 连接关闭
+  //   socket.onclose = () => {
+  //     console.log('WebSocket 连接已关闭');
+  //     sendLoading.value = false;
+  //   };
+    
+  //   // 添加到控制器列表,以便可以在需要时中断连接
+  //   controllerList.value.push({
+  //     abort: () => {
+  //       socket.close();
+  //     }
+  //   });
+  // } catch (error) {
+  //   console.error('创建 WebSocket 失败:', error);
+  //   sendLoading.value = false;
+    
+  //   // 显示错误消息
+  //   obj.content = '连接失败,请重试';
+    
+  //   // 保存到数据库
+  //   putChat({
+  //     ...obj,
+  //     content: obj.content
+  //   });
+  // }
   
-  // 保存到数据库
-  putChat({
-    ...obj,
-    content: '' // 保存完整内容
-  })
+  // 移除原有的 EventSource 代码
+  // const params = new URLSearchParams({...});
+  // const eventSource = new EventSource(...);
+  // ...
 }
 function handleCapture() {
   ElMessage({
@@ -448,22 +595,19 @@ function handleCapture() {
     showClose: true
   })
 }
-
 function hisRecords() {
   drawerRef.value.drawer = true
 }
 const createFileObj = async (file) => {
-  let formData = new FormData()
-  formData.append('avatarFile', file)
-  handleUpload(formData,file.name)
+  handleUpload(file)
 }
-const handleUpload = async (formData,name) => {
+const handleUpload = async (file) => {
   if (type.value === FunctionList.Intelligent_Form_filling) {
     pageInfoList.value = []
   }
   isShowPage.value = true
   const obj = reactive({
-    title: name,
+    title: file.name,
     state: '上传中...',
     favIconUrl: fileLogo,
     loading: true,
@@ -476,24 +620,35 @@ const handleUpload = async (formData,name) => {
   })
   pageInfoList.value = [obj]
   try {
+    let formData = new FormData()
+    formData.append('avatarFile', file)
     const res = await uploadFile(formData)
   if (type.value === FunctionList.Intelligent_Form_filling) {
     obj.state = '解析中...'
     obj.redisKey = res.data.redisKey
     obj.id = res.data.file.id
     obj.url = res.data.file.url
-    console.log({
-      body: formInfo.value,
-      uploadFile:res.data.data
-    },888569);
-    
-    const result = await getFormKey({
+    const fileExtension = file.name.split('.').pop().toLowerCase()
+    if (fileExtension === 'xlsx') {
+          const readData = await getXlsxValue(file)
+      readData[0].forEach((header, i) => {
+        // if (!xlsxData.value[header]) xlsxData.value[header] = []
+        xlsxData.value[header] = readData[1][i]
+      })
+    } else {
+         const result = await getFormKey({
       body: formInfo.value,
-      uploadFile:res.data.data
+      input_data:res.data.data
     })
-    console.log(result);
-    
+    xlsxData.value = JSON.parse(result.data)
+      }
+    }
+ 
+   nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
   }
+})
   if (type.value === FunctionList.File_Operation) {
     obj.loading = false
     obj.url = res.data.url

+ 4 - 2
src/entrypoints/sidepanel/component/formTable.vue

@@ -17,7 +17,7 @@ const tableData = computed(() => {
   else arr = JSON.parse(props.content)
   if (arr[0].data) show.value = true
   return arr.map((_:any) => ({
-    label: _.label ?? _.findByValue,
+    label: !!_.label ?_.label : _.findByValue,
     source: _.excelColumn,
     value:_.data
   }))
@@ -25,12 +25,14 @@ const tableData = computed(() => {
 </script>
 
 <template>
-  <div class="mb-2" >抽取对应关系</div>
+  <div>
+    <div class="mb-2" >抽取对应关系</div>
   <el-table :data="tableData" style="width: 100%">
     <el-table-column show-overflow-tooltip prop="label" label="表单项" width="100" />
     <el-table-column show-overflow-tooltip prop="source" label="数据源" width="100" />
     <el-table-column v-if="show" show-overflow-tooltip prop="value" label="数据值" width="100" />
   </el-table>
+  </div>
 
 </template>
 

+ 5 - 3
src/entrypoints/sidepanel/hook/useMsg.ts

@@ -122,9 +122,11 @@ export function useMsg(scrollbar?: any) {
   })
     messages.value.push(obj)
     msg = msg.split('/智能填表')[1]
-    if (scrollbar.value && scrollbar.value.wrapRef) {
-      scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
-    }
+      nextTick(() => {
+        if (scrollbar.value && scrollbar.value.wrapRef) {
+          scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+        }
+      })
     if (!msg) {
       sendLoading.value = false
       const res = await awaitFindForm(obj)

+ 40 - 34
src/store/modules/msg.ts

@@ -10,48 +10,54 @@ export const useMsgStore = defineStore('msg', {
     msgUuid: <string>'',
     messages: <any[]>[],
     hasNext:true,
-    page:1,
+    page: 1,
+    msgLoading:false,
     pageInfoList: [],
     AIModel: <any>{},
     openai: <any>null
   }),
   actions: {
     async initMsg() {
-      console.log(this.page,this.hasNext,this.messages);
-      const { msgUuid } = await new Promise<any>((resolve) => {
-        chrome.storage.local.get(['msgUuid'], (result) => {
-          resolve(result)
+      try {
+        const { msgUuid } = await new Promise<any>((resolve) => {
+          chrome.storage.local.get(['msgUuid'], (result) => {
+            resolve(result)
+          })
         })
-      })
-      if (msgUuid) {
-        console.log(85556);
-        this.msgUuid = msgUuid
-        const res = await getChatDetail({
-          page: this.page,
-          size: 100,
-          sort: 'sortKey,desc',
-          conversationId: this.msgUuid
-        })
-        this.messages = res.data.list.reverse().map(_ => ({
-          id:_.id,
-          type: _.type,
-          rawContent: _.rawContent,
-          senderId: _.senderId,
-          receiverId: _.receiverId,
-          sortKey: _.sortKey,
-          role: _.role,
-          conversationId: _.conversationId,
-          addToHistory: _.addToHistory,
-          content: _.role === 'system' ?  formatMessage(_.rawContent) : _.content
-        })).concat(this.messages)
-        if (this.messages.length < res.data.total) {
-          this.hasNext = true
-          this.page++
+        if (msgUuid) {
+          this.msgLoading = true
+          this.msgUuid = msgUuid
+          const res = await getChatDetail({
+            page: this.page,
+            size: 100,
+            sort: 'sortKey,desc',
+            conversationId: this.msgUuid
+          })
+          this.messages = res.data.list.reverse().map(_ => ({
+            id: _.id,
+            type: _.type,
+            rawContent: _.rawContent,
+            senderId: _.senderId,
+            receiverId: _.receiverId,
+            sortKey: _.sortKey,
+            role: _.role,
+            conversationId: _.conversationId,
+            addToHistory: _.addToHistory,
+            content: _.role === 'system' ? formatMessage(_.rawContent) : _.content
+          })).concat(this.messages)
+          if (this.messages.length < res.data.total) {
+            this.hasNext = true
+            this.page++
+          }
+          else this.hasNext = false
+        } else {
+          this.msgUuid = uuidv4()
+          chrome.storage.local.set({ msgUuid: this.msgUuid })
         }
-        else this.hasNext = false
-      } else {
-        this.msgUuid = uuidv4()
-        chrome.storage.local.set({ msgUuid: this.msgUuid })
+      } catch (error) {
+        
+      } finally {
+        this.msgLoading = false
       }
     },
 

+ 0 - 2
src/utils/request.js

@@ -8,9 +8,7 @@ axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
 // 创建axios实例
 const service = axios.create({
   baseURL: import.meta.env.VITE_APP_BASE_API,
-  timeout: 60000
 })
-console.log(import.meta.env.VITE_APP_BASE_API);
 
 let cancel
 // request拦截器