Ver Fonte

Merge remote-tracking branch 'origin/wzg' into chd

chd há 5 meses atrás
pai
commit
a10a4e2611

+ 51 - 37
src/entrypoints/sidepanel/Chat.vue

@@ -9,12 +9,12 @@
           <el-avatar :size="32" :src="message.avatar" />
           <div class="message-content">
             <div class="content" v-if="!message.isSelf" :class="{ 'loading-content': message.content === '' }">
-              <div v-html="message.content"></div>
-              <div class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
+              <span v-html="message.content"></span>
+              <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
                 <span class="dot"></span>
                 <span class="dot"></span>
                 <span class="dot"></span>
-              </div>
+              </span>
             </div>
             <div v-else class="content">{{ message.content }}</div>
             <div class="timestamp ">{{ message.timestamp }}
@@ -25,7 +25,7 @@
       </div>
     </el-scrollbar>
 
-    <Tools @read-click="isShowPage = true, taklToHtml = true" @upload-file="handleUpload" />
+    <Tools @read-click="readClick" @upload-file="handleUpload" @handle-capture="handleCapture" />
 
     <div>
       <!-- 输入区域 -->
@@ -73,8 +73,6 @@
 <script setup>
 import { ref, onMounted, nextTick, inject, useTemplateRef } from 'vue'
 import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
-import avator from '@/public/icon/32.png'
-import moment from 'moment'
 import { buildExcelUnderstandingPrompt } from '@/utils/ai-service.js'
 import * as XLSX from "xlsx";
 import { ElMessage } from 'element-plus';
@@ -87,7 +85,6 @@ import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResiz
 
 // 滚动条引用
 const scrollbar = ref(null);
-const type = ref('');
 const xlsxData = ref({});
 const isShowPage = ref(false);
 const tareRef = useTemplateRef("textareaRef");
@@ -98,12 +95,13 @@ const {
   taklToHtml,
   pageInfo,
   sendLoading,
+  type,
   addMessage,
   sendRequese,
   getPageInfo,
   streamRes,
   handleInput
-} = useMsg(scrollbar, type, xlsxData, fetchDataAndProcess);
+} = useMsg(scrollbar, xlsxData, fetchDataAndProcess);
 const { handleCardButtonClick } = useSummary(addMessage, sendRequese);
 
 function handelIntelligentFillingClick() {
@@ -115,11 +113,25 @@ function handelIntelligentFillingClick() {
   }
 }
 
+async function readClick() {
+  isShowPage.value = true;
+  taklToHtml.value = true;
+  await getPageInfo()
+}
+
 function handleAsk() {
   addMessage(inputMessage.value.trim(), true);
   inputMessage.value = '';
 }
 
+function handleCapture() {
+  ElMessage({
+    message: '开发中...',
+    grouping: true,
+    showClose: true
+  });
+}
+
 // 计算标题是否需要滚动
 const titleScroll = computed(() => {
   return pageInfo.value?.title?.length > 20 // 当标题超过20个字符时触发滚动
@@ -197,8 +209,7 @@ const handleUpload = (file) => {
 async function fetchDataAndProcess(input, obj) {
   console.log(input);
 
-  const pageInfo = await getPageInfo()
-  console.log(pageInfo);
+  const pageInfo = await getPageInfo();
 
   const res = await hepl({
       input_data: input,
@@ -283,7 +294,6 @@ onMounted(async () => {
       pageInfo.value = message.data
     }
   });
-  await getPageInfo()
 
   nextTick(() => {
     scrollbar.value?.setScrollTop(99999)
@@ -493,43 +503,47 @@ onMounted(async () => {
 }
 
 .loading-indicator {
-  display: flex;
+  display: inline-block;
   align-items: center;
   gap: 4px;
-}
-
-.dot {
-  width: 8px;
-  height: 8px;
-  background-color: #409eff;
-  border-radius: 50%;
-  display: inline-block;
-  animation: pulse 1.5s infinite ease-in-out;
-}
+  margin-left: 2px;
+
+  .dot {
+    width: 4px;
+    height: 4px;
+    margin: 0 2px;
+    background-color: gray;
+    border-radius: 50%;
+    display: inline-block;
+    animation: pulse 1.5s infinite ease-in-out;
+  }
 
-.dot:nth-child(2) {
-  animation-delay: 0.3s;
-}
+  .dot:nth-child(2) {
+    animation-delay: 0.3s;
+  }
 
-.dot:nth-child(3) {
-  animation-delay: 0.6s;
-}
+  .dot:nth-child(3) {
+    animation-delay: 0.6s;
+  }
 
-@keyframes pulse {
+  @keyframes pulse {
 
-  0%,
-  100% {
-    transform: scale(0.8);
-    opacity: 0.6;
-  }
+    0%,
+    100% {
+      transform: scale(0.8);
+      opacity: 0.6;
+    }
 
-  50% {
-    transform: scale(1.2);
-    opacity: 1;
+    50% {
+      transform: scale(1.2);
+      opacity: 1;
+    }
   }
 }
 
 
+
+
 .input-area {
   padding: 8px 10px;
   color: black;

+ 2 - 2
src/entrypoints/sidepanel/component/tools.vue

@@ -5,7 +5,7 @@ import { Reading, Upload, Paperclip, Scissor } from "@element-plus/icons-vue";
 
 const value = ref(options[0].value);
 
-const emit = defineEmits(['readClick', 'uploadFile'])
+const emit = defineEmits(['readClick', 'uploadFile','handleCapture'])
 </script>
 
 <template>
@@ -26,7 +26,7 @@ const emit = defineEmits(['readClick', 'uploadFile'])
     </el-upload>
     <span class="separator"></span>
     <el-tooltip effect="dark" content="截屏" placement="top">
-      <el-button :icon="Scissor" circle />
+      <el-button :icon="Scissor" circle  @click="emit('handleCapture')"/>
     </el-tooltip>
   </div>
 </template>

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

@@ -3,12 +3,13 @@ import { ref, reactive } from 'vue';
 import avator from '@/public/icon/32.png';
 import moment from 'moment'
 // import { sendMessage } from '@/utils/ai-service';
-export function useMsg(scrollbar: any, type: any, xlsxData: any, fetchDataAndProcess: Function) {
+export function useMsg(scrollbar: any, xlsxData: any, fetchDataAndProcess: Function) {
   const inputMessage = ref('');
   const indexTemp = ref(0);
   const taklToHtml = ref<any>(false);
   const sendLoading = ref(false);
   const pageInfo = ref<any>({});
+  const type = ref('');
   const formMap = ref([]);
   const messages = ref([{
     username: '用户1',
@@ -176,6 +177,7 @@ export function useMsg(scrollbar: any, type: any, xlsxData: any, fetchDataAndPro
     pageInfo,
     sendLoading,
     formMap,
+    type,
     addMessage,
     sendRequese,
     getPageInfo,

+ 153 - 103
src/utils/page-analyzer.js

@@ -1,123 +1,173 @@
 // 确保类定义在全局作用域
 export default class PageAnalyzer {
-    constructor() {
-        this.readability = null;
+  constructor() {
+    this.readability = null;
+  }
+
+  /**
+   * 分析页面内容
+   * @returns {Object} 页面分析结果
+   */
+  analyzePage(iframe = false) {
+    try {
+      // 检查Readability是否可用
+      if (typeof Readability === "undefined") {
+        console.warn(
+          "Readability not loaded, falling back to basic extraction"
+        );
+        return this.fallbackAnalysis(iframe);
+      }
+
+      // 创建文档副本以避免修改原始DOM
+      const documentClone = document.cloneNode(true);
+
+      // 初始化 Readability
+      this.readability = new Readability(documentClone, {
+        debug: false,
+        charThreshold: 20,
+      });
+
+      // 解析页面
+      const article = this.readability.parse();
+
+      return {
+        title: article.title || document.title,
+        url: window.location.href,
+        mainContent: article.textContent || article.excerpt || "",
+        excerpt: article.excerpt || "",
+        siteName: article.siteName || new URL(window.location.href).hostname,
+        wordCount: article.length || 0,
+      };
+    } catch (error) {
+      console.warn("Readability failed, using fallback:", error);
+      return this.fallbackAnalysis();
     }
-
-    /**
-     * 分析页面内容
-     * @returns {Object} 页面分析结果
-     */
-    analyzePage(iframe = false) {
-        try {
-            // 检查Readability是否可用
-            if (typeof Readability === "undefined") {
-                console.warn(
-                    "Readability not loaded, falling back to basic extraction"
-                );
-                return this.fallbackAnalysis(iframe);
-            }
-
-            // 创建文档副本以避免修改原始DOM
-            const documentClone = document.cloneNode(true);
-
-            // 初始化 Readability
-            this.readability = new Readability(documentClone, {
-                debug: false,
-                charThreshold: 20,
-            });
-
-            // 解析页面
-            const article = this.readability.parse();
-
-            return {
-                title: article.title || document.title,
-                url: window.location.href,
-                mainContent: article.textContent || article.excerpt || "",
-                excerpt: article.excerpt || "",
-                siteName: article.siteName || new URL(window.location.href).hostname,
-                wordCount: article.length || 0,
-            };
-        } catch (error) {
-            console.warn("Readability failed, using fallback:", error);
-            return this.fallbackAnalysis();
+  }
+
+  // 基础提取方法作为后备
+  fallbackAnalysis(iframe) {
+    return {
+      title: document.title,
+      url: window.location.href,
+      mainContent: this.extractMainContent(iframe),
+      excerpt: "",
+      siteName: new URL(window.location.href).hostname,
+      wordCount: 0,
+    };
+  }
+
+  // 基础的内容提取方法
+  extractMainContent(iframe) {
+    // 移除脚本、样式等
+
+    //提取页面的表单
+    if (iframe) {
+      const form = document.querySelector("form");
+      if (form) {
+        content.textContent = form.outerHTML;
+      }
+    } else {
+      const content = document.body.cloneNode(true);
+      cleanPage(content)
+      // .trim().replace(/\s+/g, " ")
+      // content
+      //     .querySelectorAll("script, style, iframe, nav, header, footer,svg")
+      //     .forEach((el) => el.remove());
+
+      const regex = /\s+/g;
+
+      // 定义标准 HTML 属性集合 排除id class style href src target
+      const standardAttributes = new Set(['alt', 'title', 'type', 'value', 'name', 'placeholder', 'disabled', 'checked', 'selected', 'readonly', 'required', 'maxlength', 'min', 'max', 'step', 'pattern', 'autocomplete', 'autofocus', 'multiple', 'rows', 'cols', 'rel', 'aria-*']);
+
+      // 创建一个临时容器
+      const temp = document.createElement('div');
+      temp.innerHTML = content.outerHTML;
+      // 递归删除空标签
+      function removeEmpty(element) {
+        const children = Array.from(element.children);
+        children.forEach(child => {
+          removeEmpty(child); // 递归处理子元素
+        });
+
+        // 检查标签是否为空
+        if (!element.innerHTML.trim()) {
+          element.remove(); // 删除空标签
         }
+      }
+
+      // 从临时容器的子元素开始处理
+      removeEmpty(temp);
+
+      // 删除 <link> 标签
+      const linkTags = temp.querySelectorAll('link');
+      linkTags.forEach(link => {
+        link.remove();
+      });
+
+      // 遍历所有元素
+      const elements = temp.querySelectorAll('*');
+      elements.forEach(element => {
+        // 获取所有属性
+        const attributes = Array.from(element.attributes);
+        attributes.forEach(attr => {
+          // 如果属性不是标准属性,则移除
+          if (!standardAttributes.has(attr.name) && !attr.name.startsWith('aria-')) {
+            element.removeAttribute(attr.name);
+          }
+        });
+      });
+
+      // 获取处理后的 HTML 字符串
+      const cleanedHtml = temp.innerHTML.trim().replace(regex, " ");
+
+      // 销毁临时容器
+      temp.remove();
+
+      console.log(cleanedHtml)
+      // content.outerHTML.trim().replace(/\s+/g, " ");
+      return cleanedHtml;
     }
 
-    // 基础提取方法作为后备
-    fallbackAnalysis(iframe) {
-        return {
-            title: document.title,
-            url: window.location.href,
-            mainContent: this.extractMainContent(iframe),
-            excerpt: "",
-            siteName: new URL(window.location.href).hostname,
-            wordCount: 0,
-        };
-    }
-
-    // 基础的内容提取方法
-    extractMainContent(iframe) {
-        // 移除脚本、样式等
-
-        //提取页面的表单
-        if (iframe) {
-            const form = document.querySelector("form");
-            if (form) {
-                content.textContent = form.outerHTML;
-            }
-        } else {
-            const content = document.body.cloneNode(true);
-            cleanPage(content)
-// .trim().replace(/\s+/g, " ")
-            // content
-            //     .querySelectorAll("script, style, iframe, nav, header, footer,svg")
-            //     .forEach((el) => el.remove());
-            console.log(content.outerHTML.trim().replace(/\s+/g, " "),56565);
-            
-            return content.outerHTML.trim().replace(/\s+/g, " ");
-        }
-
 
-    }
+  }
 };
 
 function cleanPage(body) {
 
-    // 移除所有行内样式
-    const elementsWithInlineStyle = body.querySelectorAll('[style]');
-    elementsWithInlineStyle.forEach(element => {
-        element.removeAttribute('style');
-    });
-
-    // 移除所有注释节点
-    const comments = [];
-    const walk = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT, null, false);
-    while (walk.nextNode()) {
-        comments.push(walk.currentNode);
-    }
-    comments.forEach(comment => comment.remove());
+  // 移除所有行内样式
+  const elementsWithInlineStyle = body.querySelectorAll('[style]');
+  elementsWithInlineStyle.forEach(element => {
+    element.removeAttribute('style');
+  });
+
+  // 移除所有注释节点
+  const comments = [];
+  const walk = document.createTreeWalker(body, NodeFilter.SHOW_COMMENT, null, false);
+  while (walk.nextNode()) {
+    comments.push(walk.currentNode);
+  }
+  comments.forEach(comment => comment.remove());
 
-    // 移除所有SVG图标
-    body.querySelectorAll("script, style, iframe, nav, header, footer,svg").forEach(svg => svg.remove());
+  // 移除所有SVG图标
+  body.querySelectorAll("script, style, iframe, nav, header, footer,svg").forEach(svg => svg.remove());
 
-    // 移除所有图片的src属性
-    const images = body.querySelectorAll('img');
-    images.forEach(img => {
-        img.removeAttribute('src');
-    });
+  // 移除所有图片的src属性
+  const images = body.querySelectorAll('img');
+  images.forEach(img => {
+    img.removeAttribute('src');
+  });
 
-    // 可选择移除其他无关内容,比如脚本和广告等
-    // const scripts = body.querySelectorAll('script');
-    // scripts.forEach(script => script.remove());
+  // 可选择移除其他无关内容,比如脚本和广告等
+  // const scripts = body.querySelectorAll('script');
+  // scripts.forEach(script => script.remove());
 
-    // const iframes = body.querySelectorAll('iframe');
-    // iframes.forEach(iframe => iframe.remove());
+  // const iframes = body.querySelectorAll('iframe');
+  // iframes.forEach(iframe => iframe.remove());
 
-    const ads = body.querySelectorAll('.ad, .advertisement, .ads');
-    ads.forEach(ad => ad.remove());
+  const ads = body.querySelectorAll('.ad, .advertisement, .ads');
+  ads.forEach(ad => ad.remove());
 
-    console.log('页面清理完成!');
+  console.log('页面清理完成!');
 }