瀏覽代碼

fix(sidepanel): 优化智能填表功能

- 修改了文件上传和解析的逻辑,增加了错误处理
- 优化了表单匹配和填充的算法,提高了准确性和效率
- 改进了用户交互体验,增加了加载状态和错误提示
- 重构了部分代码结构,提高了可维护性和可扩展性
chd 5 月之前
父節點
當前提交
1858f1f321

+ 2 - 2
src/entrypoints/content.js

@@ -158,7 +158,7 @@ export default defineContentScript({
                 if (label) {
                   const input = findLabelForInput(label)
                   if (input) {
-                    await simulateUserInput(input, excelDataA[item.excelColumn])
+                    await simulateUserInput(input, excelDataA[item.content])
                   }
                 }
               }
@@ -267,7 +267,7 @@ export default defineContentScript({
               await simulateUserInput(input, excelDataA[item.excelColumn])
             }
           }
-          if (item.type === 'select') {
+          if (item.type === 'select' || item.type === "cascader") {
             const input = findLabelForInput(label)
             if (input) {
               await simulateUserInput(input, excelDataA[item.excelColumn])

+ 49 - 24
src/entrypoints/sidepanel/Chat.vue

@@ -8,11 +8,12 @@
     <el-scrollbar v-else class="message-list" ref="scrollbar">
       <div class="messages">
         <div v-for="(message, index) in messages" :key="index"
-             :class="['message-item', message.isSelf ? 'self' : 'other']">
+          :class="['message-item', message.isSelf ? 'self' : 'other']">
           <el-avatar :size="32" :src="message.avatar" />
           <div class="message-content">
             <div class="content" v-if="!message.isSelf" :class="{ 'loading-content': message.content === '' }">
-              <span v-html="message.content"></span>
+              <formTable v-if="message.type === 'form'" :content="message.rawContent" />
+              <span v-else v-html="message.content"></span>
               <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
                 <span class="dot"></span>
                 <span class="dot"></span>
@@ -20,7 +21,7 @@
               </span>
             </div>
             <document v-else-if="message.type === 'document' && message.isSelf" :content="message.content"
-                      :rawContent="message.rawContent" />
+              :rawContent="message.rawContent" />
             <div v-else class="content">
               <span v-if="message.type === ''">{{ message.content }}</span>
               <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
@@ -37,13 +38,9 @@
       </div>
     </el-scrollbar>
 
-    <Tools @read-click="readClick"
-           @upload-file="handleUpload"
-           @handle-capture="handleCapture"
-           @his-records="hisRecords"
-           @add-new-dialogue="addNewDialogue"
-           @handle-current-change="handleCurrentChange"
-           @handel-intelligent-filling-click="handelIntelligentFillingClick" />
+    <Tools @read-click="readClick" @upload-file="handleUpload" @handle-capture="handleCapture" @his-records="hisRecords"
+      @add-new-dialogue="addNewDialogue" @handle-current-change="handleCurrentChange"
+      @handel-intelligent-filling-click="handelIntelligentFillingClick" />
 
     <div>
       <!-- 输入区域 -->
@@ -51,7 +48,7 @@
         <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
           <div class="card_list">
             <div v-for="(v,i) in pageInfoList" :key="i"
-                 :class="`card-content ${pageInfoList.length > 1 ? 'card_width' : ''}`">
+              :class="`card-content ${pageInfoList.length > 1 ? 'card_width' : ''}`">
               <img :src="v?.favIconUrl" style="width: 24px;display: block" />
               <div class="title-wrapper">
                 <span class="els title-scroller">{{ v?.title }}</span>
@@ -77,7 +74,7 @@
         </div>
 
         <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
-                  @keyup.enter="handleAsk" />
+          @keyup.enter="handleAsk" />
         <div class="chat_area_op">
           <el-button type="primary" link @click="handleAsk" :disabled="!inputMessage.trim() || sendLoading">
             <el-icon size="18" :color="inputMessage.trim() ? 'black' : 'gray'">
@@ -114,6 +111,8 @@ import Tools from '@/entrypoints/sidepanel/component/tools.vue'
 import historyComponent from '@/entrypoints/sidepanel/component/historyComponent.vue'
 import document from '@/entrypoints/sidepanel/component/document.vue'
 import pageMask from '@/entrypoints/sidepanel/component/pageMask.vue'
+import formTable from '@/entrypoints/sidepanel/component/formTable.vue'
+
 import { mockData, startMsg, mockData2, options, FunctionList } from '@/entrypoints/sidepanel/mock'
 import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts'
 import { getPageInfo, getXlsxValue, handleInput } from './utils/index.js'
@@ -220,7 +219,7 @@ async function handelIntelligentFillingClick() {
   taklToHtml.value = true
   const tempPageInfo = await getPageInfo()
   pageInfo.value = tempPageInfo
-  pageInfoList.value.unshift(tempPageInfo)
+  pageInfoList.value = [tempPageInfo]
   inputMessage.value = '/智能填表 '
   type.value = FunctionList.Intelligent_Form_filling
 }
@@ -273,6 +272,7 @@ function deletePageInfo(i) {
   if (pageInfoList.value.length === 0) {
     isShowPage.value = false
     taklToHtml.value = false
+    if (type.value === FunctionList.Intelligent_Form_filling) inputMessage.value = ''
   }
 }
 
@@ -297,12 +297,19 @@ async function handleAsk() {
       keyPath: 'id'
     })
   }
-  addMessage(inputMessage.value.trim())
+  addMessage(str)
   if (type.value === FunctionList.Intelligent_Form_filling) {
-    const res = await fetchRes(inputMessage.value.trim())
+    const res = await fetchRes(str)
+    console.log(res);
+    
     if (res.status === 'ok') {
       formInfo.value = res.data
       console.log(res)
+    } else {
+      type.value = FunctionList.File_Operation
+      isShowPage.value = false
+      taklToHtml.value = false
+      pageInfoList.value = []
     }
   } else streamRes(taklToHtml.value)
   inputMessage.value = ''
@@ -342,18 +349,36 @@ const handleUpload = async (file) => {
         xlsxData.value[header] = readData[1][i]
       })
       addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, formInfo.value))
-      const a = await streamRes()
-      console.log(a)
-      handleInput(xlsxData.value, JSON.parse(a.split('json')[1].split('```')[0]))
+      const {rawContent,status} = await streamRes()
+    if (status === 'ok') {
+        let form = []
+      if (rawContent.includes('json')) form = JSON.parse(rawContent.split('json')[1].split('```')[0])
+      else form = JSON.parse(rawContent)
+      handleInput(xlsxData.value, form)
+    } 
     } else {
-      const { data, msg } = await getFileValue(file)
-      const res2 = await getFormKeyAndValue(data, formInfo.value)
-      xlsxData.value = res2.data
-      msg.rawContent = buildObjPrompt(res2.data, formInfo.value)
-      const a = await streamRes()
-      handleInput(xlsxData.value, JSON.parse(a.split('json')[1].split('```')[0]))
+     try {
+       const { data, msg } = await getFileValue(file)
+       const res2 = await getFormKeyAndValue(data, formInfo.value)
+       xlsxData.value = res2.data
+       msg.rawContent = buildObjPrompt(res2.data, formInfo.value)
+       const { rawContent,status } = await streamRes()
+       if (status === 'ok') {
+         let form = []
+         if (rawContent.includes('json')) form = JSON.parse(rawContent.split('json')[1].split('```')[0])
+         else form = JSON.parse(rawContent)
+         handleInput(xlsxData.value, form)
+       } 
+     } catch (error) {
+       msg.content = '文件解析出错'
+     } finally {
+      sendLoading.value = false
+     }
     }
     type.value = FunctionList.File_Operation
+    isShowPage.value = false
+    taklToHtml.value = false
+    pageInfoList.value = []
     return
   }
   if (type.value === FunctionList.File_Operation) {

+ 80 - 0
src/entrypoints/sidepanel/component/formTable.vue

@@ -0,0 +1,80 @@
+<script setup lang="ts">
+import { onMounted, computed,ref } from 'vue'
+
+const props = defineProps({
+  content: {
+    type: String,
+    default: ''
+  },
+
+})
+const show = ref(false)
+const tableData = computed(() => {
+  let arr = []
+  if (props.content.includes('json')) {
+    arr = JSON.parse(props.content.split('json')[1].split('```')[0])
+  }
+  else arr = JSON.parse(props.content)
+  if (arr[0].data) show.value = true
+  return arr.map((_:any) => ({
+    label: _.label ?? _.findByValue,
+    source: _.excelColumn,
+    value:_.data
+  }))
+})
+</script>
+
+<template>
+  <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>
+
+</template>
+
+<style scoped lang="scss">
+.els {
+  white-space: nowrap; /* 强制文本不换行 */
+  overflow: hidden; /* 隐藏溢出内容 */
+  text-overflow: ellipsis; /* 显示省略号 */
+}
+
+.document_r {
+  display: flex;
+  flex-direction: column;
+  align-items: end;
+}
+
+.document_content {
+  width: 240px;
+  height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: start;
+  padding: 8px 8px;
+  background-color: rgba(255, 255, 255, 0.3);
+  border: 1px solid rgba(102, 102, 102, 0.3);
+  border-radius: 6px;
+  margin-bottom: 6px;
+
+  .document_img {
+    margin-right: 8px;
+    height: 32px;
+  }
+
+  .document_text {
+    flex-shrink: 0;
+    width: calc(100% - 56px);
+  }
+}
+
+.document_content1 {
+  width: fit-content;
+  max-width: 240px;
+  padding: 10px 12px;
+  border-radius: 6px;
+  background-color: rgba(255, 255, 255, 0.8);
+}
+</style>

+ 65 - 49
src/entrypoints/sidepanel/hook/useMsg.ts

@@ -111,7 +111,6 @@ export function useMsg(scrollbar?: any) {
           }
           if (status === 'select') {
             obj.content = '检测到左侧页面中有多个表单,请选择要填写的表单。'
-
             function handle(message, sender, sendResponse) {
               if (message.type === 'TO_SIDE_PANEL_FORM_INFO') {
                 console.log('收到一次性消息:', message.data)
@@ -150,21 +149,28 @@ export function useMsg(scrollbar?: any) {
       const res = await awaitFindForm(obj)
       return res
     }
-    const res = await fetchDataAndProcess(msg, obj)
-    type.value = ''
-    sendLoading.value = false
-    if (res.status === 'ok') {
-      await new Promise((res: any) =>
-        setTimeout(() => {
-          res()
-        }, 2000)
-      )
-      const res = await awaitFindForm(obj)
-      return res
-    }
+   try {
+     const res = await fetchDataAndProcess(msg, obj)
+     sendLoading.value = false
+     if (res.status === 'ok') {
+       await new Promise((res: any) =>
+         setTimeout(() => {
+           res()
+         }, 2000)
+       )
+       const res = await awaitFindForm(obj)
+       console.log(res,34444);
+       
+       return res
+     }
+   } catch (error) {
+     obj.content = '流程链执行出错'
+     return {status: 'error'}
+   } finally {
+     sendLoading.value = false
+   }
   }
   let str = ''
-
   async function fetchDataAndProcess(input: any, obj: any) {
     str = input
     console.log(str)
@@ -174,20 +180,20 @@ export function useMsg(scrollbar?: any) {
         res()
       }, 2000)
     )
-    const res = await hepl({
-      input_data: input,
-      body: pageInfo.content.mainContent
-    })
-    // const res: any = await new Promise((resolve, reject) => {
-    //   setTimeout(() => {
-    //     resolve({
-    //       data:
-    //         pageInfo.title === '智能招采'
-    //           ? mockData[indexTemp.value]
-    //           : mockData2[indexTemp.value]
-    //     })
-    //   }, 1000)
+    // const res = await hepl({
+    //   input_data: input,
+    //   body: pageInfo.content.mainContent
     // })
+    const res: any = await new Promise((resolve, reject) => {
+      setTimeout(() => {
+        resolve({
+          data:
+            pageInfo.title === '智能招采'
+              ? mockData[indexTemp.value]
+              : mockData2[indexTemp.value]
+        })
+      }, 1000)
+    })
     if (!res.data.tag || res.data.tag === 'undefined') {
       ElMessage({
         message: '未找到标签,请重试',
@@ -242,6 +248,8 @@ export function useMsg(scrollbar?: any) {
       id: messages.value.length + 1,
       username: '用户1',
       content: '',
+      type: '', // form 用于展示抽取的内容
+      data:'',
       rawContent: '', // 存储原始内容
       timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
       isSelf: false,
@@ -274,28 +282,36 @@ export function useMsg(scrollbar?: any) {
     nextTick(() => {
       scrollbar.value?.setScrollTop(99999)
     })
-    const iterator = await sendMessage(history)
-    for await (const chunk of iterator) {
-      if (chunk) {
-        const decodedChunk = chunk.choices[0].delta.content
-        if (decodedChunk) {
-          // 保存原始内容
-          obj.rawContent += decodedChunk
-          // 实时格式化显示内容
-          obj.content = formatMessage(obj.rawContent)
-          // if (type.value === '2') obj.content = obj.content.replace(/item/g, '表单项').replace(/excelColumn/g, '对应数据源')
-        }
-      }
-      scrollbar.value?.setScrollTop(99999)
-    }
-    //添加到存储历史
-    useStore(msgUuid.value).add({ ...obj })
-    // 处理最终内容
-    sendLoading.value = false
-    nextTick(() => {
-      scrollbar.value?.setScrollTop(99999)
-    })
-    return obj.rawContent
+   try {
+     const iterator = await sendMessage(history)
+     for await (const chunk of iterator) {
+       if (chunk) {
+         const decodedChunk = chunk.choices[0].delta.content
+         if (decodedChunk) {
+           // 保存原始内容
+           obj.rawContent += decodedChunk
+           // 实时格式化显示内容
+           obj.content = formatMessage(obj.rawContent)
+         }
+       }
+       scrollbar.value?.setScrollTop(99999)
+     }
+     if (type.value === FunctionList.Intelligent_Form_filling) {
+       obj.type = 'form' //
+     }
+     return {rawContent:obj.rawContent,status:'ok'}
+   } catch (error) {
+     obj.content = '网络出错'
+     return { rawContent: obj.rawContent, status: 'errora' }
+   } finally {
+     //添加到存储历史
+     useStore(msgUuid.value).add({ ...obj })
+     // 处理最终内容
+     sendLoading.value = false
+     nextTick(() => {
+       scrollbar.value?.setScrollTop(99999)
+     })
+   }
   }
 
   async function requestFlowFn(data: any[]) {

+ 9 - 9
src/entrypoints/sidepanel/utils/ai-service.js

@@ -228,9 +228,9 @@ ${pageInfo}
 1. 请根据表单中的表单项和列标题进行匹配,一定以表单为准,列标题只能匹配一次,没有和列标题匹配到的表单项不返回!
 2. 生成表单项与列标题对应的数组,并使用findBy告诉我通过表单项的什么字段信息匹配到的,使用findByValue告诉我匹配到的表单项字段值,使用excelColumn字段告诉我excel文件中列标题的值。
 3. 表单项有id根据id匹配,findBy是id,并通过findByValue告诉我id的值,没有id根据label匹配,findBy是label,并通过findByValue给我label元素的值,没有label根据placeholder匹配,findBy是placeholder,并通过findByValue告诉我placeholder的值,没有placeholder,再根据其他内容匹配
-3. 并去除没有匹配到的表单项和excel文件中没有匹配到的列,
-4. 通过type字段告诉我输入项的类型,表单项有label,通过label字段返回label的值。
-5. 返回json格式数组,不要返回其他任何内容`
+4. 并去除没有匹配到的表单项和excel文件中没有匹配到的列,
+5. 通过type字段告诉我输入项的类型,表单项有label,通过label字段返回label的值。
+. 返回json格式数组,不要返回其他任何内容`
 }
 
 // 5. 如果表单项有label标签,同时返回label, 通过label字段告诉我label元素的文本
@@ -242,13 +242,13 @@ export function buildObjPrompt(obj, pageInfo) {
 ${pageInfo}
              
 要求:
-1. 请根据表单中的表单项和对象进行匹配,一定以表单为准,key同时在对象和表单中出现,才能返回!
-2. 表单项有label,根据label匹配,没有label根据placeholder匹配,没有placeholder,根据id匹配,再根据其他内容匹配
-3. 并根据实际可操作的表单项的所有信息与上传的对象的key进行匹配,生成表单项与key的数组,并使用findBy告诉我通过表单项的什么字段信息匹配到的,使用findByValue告诉我匹配到的表单项字段值,使用excelColumn字段告诉我对应的key值。在一个字段内返回
-4. 并去除没有匹配到的表单项和对象中没有匹配到的key,
-5. 通过type字段告诉我输入项的类型,如果对象中key对应的是日期,type统一返回date。
+1. 请根据表单中的表单项和对象的key进行匹配,一定以表单为准,对象的key只能匹配一次,使用data字段返回匹配到的key对应的value值。没有和对象匹配到的表单项不返回!
+2. 生成表单项与对象的key对应的数组,并使用findBy告诉我通过表单项的什么字段信息匹配到的,使用findByValue告诉我匹配到的表单项字段值,使用excelColumn字段告诉我对象key的值。
+3. 表单项有label,根据label匹配,没有label根据placeholder匹配,没有placeholder,根据id匹配,再根据其他内容匹配
+4. 表单中没有和对象的key匹配到的表单项不要返回!。
+5. 通过type字段告诉我输入项的类型,如果对象中key对应的是日期,type统一date。
 6. 表单项有label,通过label字段返回label的值。
-7. 返回json格式返回数组,不要返回其他任何内容`
+7. 将匹配到的所有对象放在数组中返回,返回json格式返回数组,不要返回其他任何内容`
 }
 
 function escapeHtml(html) {

+ 148 - 8
src/utils/contentUtils.js

@@ -1,12 +1,113 @@
 export function formatDate(date) {
-  // 直接创建北京时间的日期对象
-  const d = new Date(date)
+  // 检查日期是否为空
+  if (!date) {
+    return '';
+  }
+  
+  let d;
+  
+  // 处理各种日期格式
+  if (typeof date === 'string') {
+    // 处理中文年月日格式:YYYY年M月D日 或 YYYY年M月
+    if (date.includes('年')) {
+      const yearMatch = date.match(/(\d+)年/);
+      const monthMatch = date.match(/(\d+)月/);
+      const dayMatch = date.match(/(\d+)日/);
+      
+      if (yearMatch && monthMatch) {
+        const year = yearMatch[1];
+        const month = String(parseInt(monthMatch[1])).padStart(2, '0');
+        // 如果有日,使用日,否则默认为1日
+        const day = dayMatch ? String(parseInt(dayMatch[1])).padStart(2, '0') : '01';
+        d = new Date(`${year}-${month}-${day}`);
+      } else {
+        d = new Date(date);
+      }
+    }
+    // 处理特殊格式:YYYY.M.D 或 YYYY.M
+    else if (date.includes('.')) {
+      const parts = date.split('.');
+      if (parts.length >= 2) {
+        // 将 YYYY.M.D 或 YYYY.M 转换为 YYYY-MM-DD 格式
+        const year = parts[0];
+        const month = String(parseInt(parts[1])).padStart(2, '0');
+        // 如果有日,使用日,否则默认为1日
+        const day = parts.length > 2 ? String(parseInt(parts[2])).padStart(2, '0') : '01';
+        d = new Date(`${year}-${month}-${day}`);
+      } else {
+        d = new Date(date);
+      }
+    }
+    // 处理斜杠分隔格式:YYYY/M/D 或 D/M/YYYY
+    else if (date.includes('/')) {
+      const parts = date.split('/');
+      if (parts.length === 3) {
+        // 判断是 YYYY/MM/DD 还是 DD/MM/YYYY
+        if (parts[0].length === 4) {
+          // YYYY/MM/DD
+          const year = parts[0];
+          const month = String(parseInt(parts[1])).padStart(2, '0');
+          const day = String(parseInt(parts[2])).padStart(2, '0');
+          d = new Date(`${year}-${month}-${day}`);
+        } else if (parts[2].length === 4) {
+          // DD/MM/YYYY
+          const year = parts[2];
+          const month = String(parseInt(parts[1])).padStart(2, '0');
+          const day = String(parseInt(parts[0])).padStart(2, '0');
+          d = new Date(`${year}-${month}-${day}`);
+        } else {
+          d = new Date(date);
+        }
+      } else {
+        d = new Date(date);
+      }
+    }
+    // 处理短横线分隔格式:YYYY-M-D
+    else if (date.includes('-')) {
+      const parts = date.split('-');
+      if (parts.length === 3) {
+        const year = parts[0];
+        const month = String(parseInt(parts[1])).padStart(2, '0');
+        const day = String(parseInt(parts[2])).padStart(2, '0');
+        d = new Date(`${year}-${month}-${day}`);
+      } else {
+        d = new Date(date);
+      }
+    }
+    // 处理纯数字格式:YYYYMMDD
+    else if (/^\d{8}$/.test(date)) {
+      const year = date.substring(0, 4);
+      const month = date.substring(4, 6);
+      const day = date.substring(6, 8);
+      d = new Date(`${year}-${month}-${day}`);
+    }
+    // 其他字符串格式
+    else {
+      d = new Date(date);
+    }
+  } 
+  // 处理时间戳(数字)
+  else if (typeof date === 'number') {
+    // 判断是否为13位时间戳(毫秒)或10位时间戳(秒)
+    d = date > 10000000000 ? new Date(date) : new Date(date * 1000);
+  }
+  // 其他类型(如Date对象)
+  else {
+    d = new Date(date);
+  }
+  
+  // 检查日期是否有效
+  if (isNaN(d.getTime())) {
+    return '';  // 如果日期无效,返回空字符串
+  }
+  
   // 获取年、月、日
-  const year = d.getFullYear()
-  const month = String(d.getMonth() + 1).padStart(2, '0') // 月份从0开始,需要加1
-  const day = String(d.getDate()).padStart(2, '0')
-  return `${year}-${month}-${day}`
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
+  const day = String(d.getDate()).padStart(2, '0');
+  return `${year}-${month}-${day}`;
 }
+
 export async function simulateCompleteUserAction(
   clickElement,
   inputElement,
@@ -139,9 +240,48 @@ export async function simulateUserInput(element, value) {
     element.value = value
     element.blur()
     return
-  }
-  element.value = value
+    }
+    const simulateTyping = async (element, text, delay = 50) => {
+        element.focus()
+        for (let char of text) {
+            await new Promise((resolve) => setTimeout(resolve, delay))
+
+            // 按键按下
+            const keydownEvent = new KeyboardEvent('keydown', {
+                key: char,
+                code: `Key${char.toUpperCase()}`,
+                bubbles: true,
+                cancelable: true
+            })
+            element.dispatchEvent(keydownEvent)
 
+            // 更新输入值
+            element.value += char
+
+            // 触发输入事件
+            const inputEvent = new InputEvent('input', {
+                bubbles: true,
+                cancelable: true,
+                data: char,
+                inputType: 'insertText'
+            })
+            element.dispatchEvent(inputEvent)
+
+            // 按键弹起
+            const keyupEvent = new KeyboardEvent('keyup', {
+                key: char,
+                code: `Key${char.toUpperCase()}`,
+                bubbles: true,
+                cancelable: true
+            })
+            element.dispatchEvent(keyupEvent)
+        }
+
+        // 触发change事件
+        element.dispatchEvent(new Event('change', { bubbles: true }))
+    }
+  element.value = value
+    // simulateTyping(element,value,50)
   // 创建并触发 input 事件
   const inputEvent = new Event('input', { bubbles: true })
   element.dispatchEvent(inputEvent)