耗时一天写了个 Search GPT 浏览器插件,提升搜索效率!

耗时一天写了个 Search GPT 浏览器插件,提升搜索效率!

前言

很多人应该都使用过微软的 Chat Bing。当使用 Bing 进行搜索的时候,不仅会展示检索的结果页,而且还会在浏览页面的侧边栏显示类似于 ChatGPT 的响应。这个时候搜索内容其实是作为 prompt 输入给 Chat Bing。如下图所示:

于是,我想着可不可以在百度或谷歌浏览器中也实现这样的效果。最直接的一个想法就是开发一个浏览器插件,赋予其 GPT 的能力。

除此之外,我还想添加一些额外的功能。每次搜索时,我们都会点击最匹配的结果页,并查看这些结果页是否符合预期。如果我们把这一步交给 GPT 去做,让它帮我们总结网页的内容,这样可以节省一些检索的时间,提高查找效率。

好嘞,简单总结一下需求,有两点:

  • 实现类似于 Chat Bing 的效果。
  • 实现总结网页内容的功能。

需求清晰后,我用了大概一天时间写了 Search GPT 浏览器插件。

接下来,我带大家看看我的实现过程!

Github 地址如下,其中包含详细的 README 文档

github.com/ltyzzzxxx/s...

技术选型

为了快速开发浏览器插件,我选择了 Plasmo 框架。主要原因是它支持 React、Vue 等框架,且能够很方便的引入第三方组件库(如 Ant Design),调试插件的时候也很方便,支持热部署。如果不使用框架的话,得写原生 html、js、css,很头疼哈哈哈哈~

选择 React 作为前端开发框架,Ant Design 作为组件库。这里就不多介绍啦,相信前端开发的同学一定非常熟悉。

为了实现这个总结页面内容这个功能,我选用了 Python + BeautifulSoup 去爬取网站内容,并由 GPT 进行总结。

Beautiful Soup 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库。它能够通过你喜欢的转换器实现惯用的文档导航 / 查找 / 修改文档的方式。Beautiful Soup 会帮你节省数小时甚至数天的工作时间。

如下为技术选型表:

技术栈 介绍
Plasmo 简化浏览器插件开发,支持使用 React、Vue 等前端开发框架
React + Ant Design 前端开发框架 + 前端组件库
Python + Flask + BeautifulSoup 后端 Web 框架 + 爬虫

浏览器插件组件

为了方便接下来介绍 Search GPT 的开发过程,我先给大家介绍一下浏览器插件中基本的组件!

在 Search GPT 中,我使用到了 Content Script、Background 与 Popup

  • Content Script:其用于直接与浏览器进行交互,它运行在页面的上下文中,可以读取和修改网页的内容。

    举个 🌰,如果我们想要实现 Chat Bing 的效果,我们需要在浏览器的右侧边栏画一个框,框中展示 ChatGPT 对搜索内容的响应。这个 UI 实现需要通过 Content Script 来完成!

  • BackGround Script:顾名思义,它是运行在插件的后台,独立于浏览器的其他网页,一般用于处理全局的状态,接收并处理 Content Script 发出的事件。

    举个🌰,当 Content Script 读取到当前页面是一个搜索页(www.google.com/search),它就可以读取搜索内容,并以事件的形式发送给 Background。Background 便可以去请求 GPT Api,获取 prompt 的响应结果,并返回给 Content Script。

  • Popup:这个组件最容易理解,当我们点击插件图标时,其会弹出一个框,可能是一个登陆框、设置框等。

代码结构

css 复制代码
├── src
│   ├── background.ts
│   ├── backgroundFunctions
│   │   ├── handleCrawlerApi.ts
│   │   └── handleGptApi.ts
│   ├── contents
│   │   ├── BaiduSearchBox.tsx
│   │   ├── GoogleSearchBox.tsx
│   │   ├── PageCollapse.tsx
│   │   └── plasmo.tsx
│   └── popup.tsx
  • background.ts:为 background 脚本文件
  • backgroundFunctions:将 background.ts 中用到的函数抽取到该文件夹中
  • contents:其中 plasmo 为 Content Script,剩余的为 React 组件
  • popup.tsx:弹出框组件

实现方案

存储 API Key

在 Search GPT 中,Popup 用于设置 API Key 密钥,并用 Plasmo 提供的 storage 进行持久化存储。

如下图所示即为 popup 效果。

其中的核心代码就是处理 Save 与 Clear 事件。

  • 保存 API Key,首先是验证 API Key 是否正确,然后进行存储,并更新提示状态。

    具体如何验证 API Key,请查看源码,原理就是调用 Open AI 提供的 api。

    typescript 复制代码
    // src/popup.ts 第 43 行
    const saveGptApiKey = async () => {
        if (await verifyGptApiKey(gptApiKey)) {
          try {
            await storage.set("gptApiKey", gptApiKey)
            setGptStatus("GPT API Key saved successfully.")
          } catch (error) {
            setGptStatus("Failed to save GPT API Key.")
          }
        }
    }
  • 清除 API Key,更新提示状态。

    typescript 复制代码
    // src/popup.ts 第 54 行
    const clearGptApiKey = async () => {
        await storage.remove("gptApiKey")
        setGptApiKey("")
        setGptStatus("Enter your GPT API Key")
    }

核心实现

需求 1

当用户用谷歌或百度搜索时,插件会以搜索内容作为 prompt,去请求 GPT。如下为具体流程图。

  • 判断页面是通过 Plasmo 框架提供的模板进行配置,只有当网站地址匹配到 google 或 baidu 才会进行之后的流程。

    typescript 复制代码
    // src/contents/plasmo.tsx 第 8 行
    export const config: PlasmoCSConfig = {
      matches: ["https://*.google.com/*", "https://*.baidu.com/*"]
    }
  • 渲染 GPT 响应框是通过 document 的方法获取到指定的位置,在该位置渲染对应的 React 组件。

    • 谷歌和百度对应的位置有所不同,所以需要分别处理
    typescript 复制代码
    // src/contents/plasmo.tsx 第 30 行
    const displayGptResponse = (responseText: string, hostname: string) => {
      let container = document.getElementById("gpt-response-container")
      if (container) {
        container.remove()
      }
      container = document.createElement("div")
      container.id = "gpt-response-container"
      if (hostname == "www.google.com") {
        let appbar = document.getElementById("appbar")
        appbar.parentNode.insertBefore(container, appbar)
        const root = createRoot(container)
        root.render(<GoogleSearchBox responseText={responseText} />)
      } else {
        const parentElement = document.getElementById("content_right")
        parentElement.insertBefore(container, parentElement.firstChild)
        const root = createRoot(container)
        root.render(<BaiduSearchBox responseText={responseText} />)
      }
    }
  • 获取用户输入的内容是通过 window 的方法,获取地址栏中对应的请求参数。

    typescript 复制代码
    // src/contents/plasmo.tsx 第 30 行
    let urlSearchParams = new URLSearchParams(window.location.search)
    const queryParam =
    window.location.hostname == "www.google.com"
      ? urlSearchParams.get("q")
      : urlSearchParams.get("wd")
  • 发送事件给 Background 是通过 chrome.runtime.sendMessage 实现的,发送的对象中有两个属性:

    • type:Background 可能会接收到多个事件,需要为事件命名以区分不同事件。
    • query:搜索内容,会作为 GPT 请求的 prompt。

    这里需要注意的是,当 GPT 请求成功或失败之后,需要再次渲染 GPT 响应框,于是再次调用 displayGptResponse 方法。

    typescript 复制代码
    // src/contents/plasmo.tsx 第 58 行
    chrome.runtime.sendMessage(
      { type: "fetchGptResponse", query: queryParam },
      (response) => {
        if (response) {
          if (response.type === "gptResponse") {
            displayGptResponse(response.data, window.location.hostname)
          } else if (response.type === "gptError") {
            console.error("GPT Error:", response.error)
            displayGptResponse("Request GPT error...", window.location.hostname)
          } else if (response.type === "apiKeyError") {
            console.error("API Key Error: API Key not set or invalid.")
            displayGptResponse("API Key not set or invalid.", window.location.hostname)
          }
        }
      }
    )
  • Background 处理逻辑为判断事件类型。如果为 GPT 请求事件,则请求 GPT,并将请求结果返回给 Content Script。

    如下为请求 GPT 代码,具体代码请查看 src/backgroundFunctions/handleGptApi.ts

    typescript 复制代码
    // src/backgroundFunctions/handleGptApi.ts
    fetch(
      "https://api.openai.com/v1/engines/text-davinci-003/completions",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: "Bearer " + apiKey
        },
        body: JSON.stringify({
          prompt: message.query,
          max_tokens: 1024
        })
      }
    )

需求 2

当用户点击每个搜索结果页下方的 collapse 时,插件会去爬取对应网页的部分内容,然后将这部分内容作为 prompt 去请求 GPT,总结网页内容。如下为具体流程图。

  • 通过 document 获取每个搜索结果页所对应的 HTML 元素,并在其下方渲染 collapse 组件。渲染方式与第一个需求相同。

    谷歌搜索和百度搜索的处理逻辑有细微差别,主要是因为 HTML 元素名不同。以 google 搜索举例:

    typescript 复制代码
    // src/contents/plasmo.tsx 第 79 行
    let parentPageElement = document.getElementById("rso")
    let i = 0
    for (let child of parentPageElement.children) {
      let targetElement = child.querySelector('[jsname="UWckNb"]')
      if (targetElement != null && targetElement.tagName === "A") {
        console.log("第" + i + "个page", targetElement.href)
        // add collapse
        let link = targetElement.href
        let container = document.createElement("div")
        container.id = "page-collapse-" + i
        const root = createRoot(container)
        child.insertAdjacentElement("afterend", container)
        root.render(<PageCollapse link={link} />)
      }
      i += 1
    }
  • 当用户点击 collapse 组件时,会展开对应的内容。此时,Content Script 会发送爬虫事件给 Background。

    typescript 复制代码
    // src/contents/PageCollapse.tsx 第 18 行
    const sendMessageToHandleCrawler = () => {
        return new Promise((resolve, reject) => {
          chrome.runtime.sendMessage(
            { type: "fetchCrawler", link: link },
            (response) => {
              if (response) {
                if (response.type === "crawlerSuccess") {
                  console.log("request crawler success", response.data)
                  setLinkContent(response.data)
                  resolve(response.data) // 解析 Promise
                } else if (response.type === "crawlerError") {
                  console.error("request crawler error", response.error)
                  reject(response.error) // 拒绝 Promise
                }
              }
            }
          )
        })
    }
  • Background 收到爬虫事件后,会去请求 Flask 后端。

    发送请求的部分代码如下,具体代码请查看 src/backgroundFunctions/handleGptApi.ts

    typescript 复制代码
    // src/backgroundFunctions/handleGptApi.ts
    fetch("http://localhost:5000/?link=" + encodeURIComponent(message.link))
        .then((resp) => {
          if (resp.status === 200) {
            return resp
              .text()
              .then((text) => sendResponse({ type: "crawlerSuccess", data: text }))
          } else {
            sendResponse({
              type: "crawlerError",
              error: "Cannot crawl the web page."
            })
          }
        })

    Python 爬虫代码如下,这里需要注意两点:

    • 网站内容过大,需要对爬取到的内容做截断,截取前 1000 个字符。
    • 有些网站无法爬取到正常的内容,需要判断内容是否为空
    python 复制代码
    @app.route('/')
    @use_kwargs({'link': fields.Str(required=True)}, location="query")
    def hello_world(link):
        response = requests.get(link)
        html = response.text
        soup = BeautifulSoup(html, "html.parser")
        text = soup.get_text()
        clean_text = text.strip().replace("\n", " ")[:1000]
        print(link, clean_text)
        if clean_text:
            return Response(clean_text, status=200)
        else:
            return Response('error', status=500)
  • Content Script 收到爬虫响应内容后,会接着给 Background 发送 GPT 请求事件,用 GPT 总结网页内容,并渲染到 collapse 框中。这里的处理逻辑与需求 1 类似,就不再赘述啦~

效果展示

对于需求 1,其实现效果如下:

对于需求 2,其实现效果如下:

总结

在这篇文章中,我从技术选型、浏览器插件组件、代码结构、实现方案、效果展示等角度,详细介绍了 Search GPT 插件的实现过程。

插件的基本功能是可以正常使用的,需要优化的点在于 GPT 的响应速度、搜索结果页的摘要准确度等方面。

代码已经开源,大家感兴趣的话可以根据 README.md 文档去试用一下这个插件。目前插件还没有发布到 chrome 商店。

如果使用过程中有 Bug 的话,麻烦提一下 Issue 呀~

Github 地址:github.com/ltyzzzxxx/s...

今天的内容就到这里啦,大家觉得有用的话麻烦帮忙点个赞、点个 Star 支持一下呀,下期再见!

相关推荐
余生H10 分钟前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿14 分钟前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~21 分钟前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫24 分钟前
高德地图自定义折线矢量图形
前端·vue.js·vue
m0_7482509326 分钟前
html 通用错误页面
前端·html
来吧~36 分钟前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
羑悻的小杀马特1 小时前
【AIGC篇】畅谈游戏开发设计中AIGC所发挥的不可或缺的作用
c++·人工智能·aigc·游戏开发
鎈卟誃筅甡1 小时前
Vuex 的使用和原理详解
前端·javascript
呆呆小雅1 小时前
二、创建第一个VUE项目
前端·javascript·vue.js
m0_748239331 小时前
前端(Ajax)
前端·javascript·ajax