vue3+vite+AI大模型实现谷歌插件-web诊断
- 一、前言
- 二、实现思路
-
- 三、技术栈简介
-
- 四、主要功能实现
- 1、Web端
- 【1】谷歌插件+vue全局配置文件
- 【2】加载web诊断工具至当前页面
- 【3】全局捕获异常错误
- 2、Server端
- 【1】websock管理模块
- 【2】调用大模型API接口
一、前言
谷歌插件-web诊断工具是基于vue3、vite和AI大模型技术的完美结合,工具致力于捕获页面报错信息,并为您提供高效的解决方案。
二、实现思路
1、功模块构图

2、数据交互图
三、技术栈简介
1、Web端
名称 | 版本 | 备注 |
---|
谷歌插件协议 | “manifest_version”: 3 | |
element-plus | ^2.9.4 | |
axios | ^1.8.4 | |
vditor | ^3.10.9 | 官网地址:https://b3log.org/vditor/ |
2、服务端
名称 | 版本 | 备注 |
---|
Node | v22.13.0 | |
express | ^5.1.0 | |
openai | ^4.91.1 | 用于大模型进行交互 |
ws | ^8.18.1 | |
四、主要功能实现
1、Web端
【1】谷歌插件+vue全局配置文件
import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({plugins: [vue(),vueDevTools(),],build: {outDir: 'dist', rollupOptions: {input: 'index.html',output: {entryFileNames: 'assets/[name].js',chunkFileNames: 'assets/[name].js',assetFileNames: 'assets/[name].[ext]'}}},base: './', resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))},},
})
{"manifest_version": 3,"name": "web 诊断","version": "1.0","description": "A browser extension to log errors.","permissions": ["tabs","activeTab","scripting","storage","webNavigation"],"action": {"default_popup": "index.html"},"background": {"service_worker": "background.js"},"host_permissions": ["http://*/*", "https://*/*"],"content_scripts": [{"matches": ["http://*/*", "https://*/*"],"js": ["contentScript.js"]}],"web_accessible_resources": [{"resources": ["index.html", "/assets/*"],"matches": ["http://*/*", "https://*/*"]}]
}
import './assets/main.css'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App);
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)
}
app.mount('#zykApp');
【2】加载web诊断工具至当前页面
实现思路: 通过contentScript文件的特性,获取当前页面的上下文,把web诊断Dom动态插入当前页面的文档流中。
文件名称: contentScript.js (谷歌插件预置文件名)
文件描述: contentScript文件是能够在网页的上下文中运行的JavaScript代码片段,
const getFilePath = (url) => {const urlSplit = url.split('/assets/');return `/assets/${urlSplit[1]}`;
}
const currentPageWindow = window;
const currentPageDocument = document;
(function() {function injectVueApp() {const appContainer = document.createElement('div');appContainer.id = 'zyk-app-container';appContainer.style.position = 'fixed'; appContainer.style.top = '48px';appContainer.style.right = '48px';appContainer.style.width = '48px';appContainer.style.height = '48px';appContainer.style.zIndex = '999999';appContainer.style.backgroundColor = 'transparent'; document.body.appendChild(appContainer);fetch(chrome.runtime.getURL('index.html')).then(response => response.text()).then(html => {const parser = new DOMParser();const doc = parser.parseFromString(html, 'text/html');const headContent = doc.head.innerHTML;const styleTags = doc.querySelectorAll('style');const linkTags = doc.querySelectorAll('link[rel="stylesheet"]');const bodyContent = doc.querySelector('#zykApp').outerHTML;if (styleTags.length > 0) {for (let i = 0; i < styleTags.length; i++) {const styleTag = document.createElement('style');styleTag.innerHTML = styleTags[i].innerHTML;document.head.appendChild(styleTag);}}if (linkTags.length > 0) {for (let i = 0; i < linkTags.length; i++) {const linkTag = document.createElement('link');linkTag.rel = 'stylesheet';linkTag.href = chrome.runtime.getURL(getFilePath(linkTags[i].href));document.head.appendChild(linkTag);}}appContainer.innerHTML = bodyContent;const scriptTags = doc.querySelectorAll('script[type="module"]');scriptTags.forEach(script => {const newScript = document.createElement('script');newScript.type = 'module';newScript.src = chrome.runtime.getURL(getFilePath(script.src));document.head.appendChild(newScript);});}).catch(error => {console.error('Failed to load Vue app HTML:', error);});}if (document.readyState === 'complete' || document.readyState === 'interactive') {injectVueApp();} else {window.addEventListener('load', injectVueApp);}
})();
【3】全局捕获异常错误
const captureErrors = () => {window.addEventListener('error', event => {console.log('error',event)errors.value.push({type: 'JavaScript Error',message: `${event.message}`,stack: `文件路径:${event.filename} [${event.lineno}:${event.colno}]`,suggestion: '', time: new Date().toLocaleString()});})window.addEventListener('unhandledrejection', event => {console.log('unhandledrejection',event)errors.value.push({type: 'Promise Rejection',message: event.reason.message,stack: event.reason.stack,suggestion: '检查 Promise 被拒绝(rejected)后没有被 .catch() 或 try...catch 处理',time: new Date().toLocaleString()});});
}
const watchHttp = () => {const originalXhrOpen = XMLHttpRequest.prototype.open;const originalXhrSend = XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open = function (method, url) {console.log('open ZYK', method, url)this._url = url; return originalXhrOpen.apply(this, arguments);};XMLHttpRequest.prototype.send = function () {this.addEventListener("load", () => {if (this.status !== 200 && this.status >= 400) {httpFailedList.value.push({url: this._url,status: this.status,time: new Date().toLocaleString(),response: this.responseText,})}});this.addEventListener("error", () => {httpFailedList.value.push({url: this._url,status: this.status,time: new Date().toLocaleString(),response: this.responseText,})});return originalXhrSend.apply(this, arguments);};const originalFetch = window.fetch;window.fetch = async function (...args) {const [url, config] = args;try {const response = await originalFetch.apply(this, args);if (!response.ok) {console.error(`Fetch 请求失败: ${url}`, response.statusText);} else {console.log(`Fetch 请求成功: ${url}`, response);}return response;} catch (error) {console.error(`Fetch 请求错误: ${url}`, error);throw error;}};
}
2、Server端
【1】websock管理模块
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {console.log('客户端已连接');ws.on('message', (message) => {const receiveMessage = formatReceiveMessage(message);if (receiveMessage.type === 'pong') {setTimeout(() => {ws.send(formatSendMessage({ type: 'ping', message: 'ping' }));}, 5 * 1000)} else {console.log(`收到客户端消息(未匹配type类型): ${message}`)}});ws.on('close', () => {ws.close();console.log('客户端已断开连接');});
});
【2】调用大模型API接口
const OpenAI = require('openai');
const express = require('express');
const WebSocket = require('ws');
const http = require('http');
const app = express();
const server = http.createServer(app);
app.use(express.json());
app.use((req, res, next) => {res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); next();
});
app.post('/zyk/api/ai', (req, res) => {const requestData = req.body;const question = `${requestData.type}:${requestData.message}`;const isExistQuestion = aiContent.find(item => item.question === question);if (isExistQuestion) {res.status(200).json({useCache: true,data: isExistQuestion.answer});} else {res.status(200).json({useCache: false});createAi(question);}
});
const openai = new OpenAI({apiKey: 'API-KEY', baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",}
);async function createAi(message) {const completion = await openai.chat.completions.create({model: "qwen-coder-plus", messages: [{ role: "system", content: "作为一个高级前端专家,你需要根据报错信息给出准确的解释,并给出修复建议" },{ role: "user", content: message }],max_tokens: 200,});aiContent.push({question: message, answer: completion.choices[0].message.content})wss.clients.forEach((client) => {if (client.readyState === WebSocket.OPEN) {client.send(formatSendMessage({ type: 'message', message: aiContent }))}})