耗时一天写了个 Search GPT 浏览器插件,提升搜索效率!
前言
很多人应该都使用过微软的 Chat Bing。当使用 Bing 进行搜索的时候,不仅会展示检索的结果页,而且还会在浏览页面的侧边栏显示类似于 ChatGPT 的响应。这个时候搜索内容其实是作为 prompt 输入给 Chat Bing。如下图所示:
于是,我想着可不可以在百度或谷歌浏览器中也实现这样的效果。最直接的一个想法就是开发一个浏览器插件,赋予其 GPT 的能力。
除此之外,我还想添加一些额外的功能。每次搜索时,我们都会点击最匹配的结果页,并查看这些结果页是否符合预期。如果我们把这一步交给 GPT 去做,让它帮我们总结网页的内容,这样可以节省一些检索的时间,提高查找效率。
好嘞,简单总结一下需求,有两点:
- 实现类似于 Chat Bing 的效果。
- 实现总结网页内容的功能。
需求清晰后,我用了大概一天时间写了 Search GPT 浏览器插件。
接下来,我带大家看看我的实现过程!
Github 地址如下,其中包含详细的 README 文档
技术选型
为了快速开发浏览器插件,我选择了 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
的方法,获取地址栏中对应的请求参数。- 对于百度搜索,
wd
参数对应的就是搜索内容。如:www.baidu.com/s?wd=你好 - 对于谷歌搜索,
q
参数对应的就是搜索内容,如:www.google.com/search?q=你好
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 支持一下呀,下期再见!