Chrome 插件开发实战:实现一个对照翻译插件(二)翻译部分

前言

上篇文章 介绍了插件的前端部分,这篇我们来介绍怎么对提取出来的内容进行翻译。生活在 ChatGPT 的时代,当然要好好利用一下这个工具了,所以本文就用它来进行翻译了。

接入 ChatGPT

本文没有采取官方 API_KEY 的方式,而是需要用户先自行登录 ChatGPT,然后利用跨站请求会自动携带 cookie 这一特性先请求 openai 的 /api/auth/session 接口获取 accessToken,然后使用其来发起对话,核心代码如下所示:

js 复制代码
  async sendMessage(params: {
    prompt: string
    onEvent: (p: OnEventParams) => void
  }) {
    if (!this.accessToken) {
      this.accessToken = await this.getAccessToken()
    }

    const rsp = await fetch(
      'https://chat.openai.com/backend-api/conversation',
      {
      ...
      }
    )

    this.parseSSEResponse(rsp, (message) => {
      console.log('msg', message)
      ...
    })
  }

由于 conversation 返回的 Content-Typetext/event-stream 类型的,所以本文使用了 eventsource-parser 来进行解析:

js 复制代码
async parseSSEResponse(resp: Response, onMessage: (message: string) => void) {
  if (!resp.ok) {
    const error = await resp.json().catch(() => null)
    if (!error) {
      throw new Error(JSON.stringify(error))
    }
    throw new Error(`${resp.status} ${resp.statusText}`)
  }
  const parser = createParser((event) => {
    if (event.type === 'event') {
      onMessage(event.data)
    }
  })
  const decoder = new TextDecoder()
  for await (const chunk of this.streamAsyncIterable(resp.body!)) {
    const str = decoder.decode(chunk)
    parser.feed(str)
  }
}

async *streamAsyncIterable(stream: ReadableStream) {
  const reader = stream.getReader()
  try {
    while (true) {
      const {done, value} = await reader.read()
      if (done) {
        return
      }
      yield value
    }
  } finally {
    reader.releaseLock()
  }
}

注意,这里的 async *streamAsyncIterable 是个 AsyncGenerator,感兴趣的可以自行学习。

但是,直接在 content.js 中运行上述代码是会跨域的,因为我们插件的 content.js 其实是运行在其他网页之中,那么怎么解决呢?答案就是代理。

首先,从插件的页面发起请求到 openai 是不会跨域的,所谓插件的页面就是类似于这样的页面:chrome-extension://dhchbfllfandgngcnkinfbdhehchbhch/index.html,其中 // 后面那一串是插件的唯一 id。所以,我们可以在网页中插入一个不可见的 iframe 来作为 Proxy,通过 postMessage 让其跟 content.js 互相进行通信:

好了,搞定了 ChatGPT 的接入,接下来就是翻译了。

翻译

上篇文章中我们已经提取出了待翻译的内容,并且按照段落组织好了,所以,接下来的任务就轻松了。我们先把每个段落的待翻译文本收集成一个数组:

js 复制代码
paragraphs.forEach(
  ({commonAncestorEl, text, translationWrapperEl, translationEl}) => {
    commonAncestorEl.appendChild(translationEl)
    texts.push(text.trim())
  }
)
const results = await this.requestAI(texts)

接下来我们写一个这样的 Prompt,发送给 ChatGPT:

json 复制代码
`I will give you a JSON array, please translate each item from ${from} to ${to} and
return a JSON array whose items are translation result string, please return the JSON directly,
here is my JSON array: ${JSON.stringify(texts)}`

其中,文本中 fromto 分别是原文语言和目标语言。然后,我们把得到的翻译结果解析出来,并显示到页面上即可:

js 复制代码
paragraphs.forEach(({translationEl}, index) => {
  translationEl.innerText = results[index]
  translationEl.classList.remove(this.loadingClass)
  translationEl.classList.add(this.translationClass)
})

总结

我们通过两篇文章介绍了如何实现一个并行翻译的 Chrome 插件,实测发现,翻译的速度有点慢,即使我们做了只翻译视口中的内容的优化。但其实还有优化空间,比如我们现在是等所有的文本都翻译好以后再一起返回结果,但 openai 的返回是流式的,我们也可以流式地进行处理。之前在编译原理之手写一门解释型语言中介绍的状态机貌似可以派上用场,即我们可以通过一个状态机来不停地匹配翻译后的文本 token,这个后面有空再优化了。最后,欢迎关注公众号"前端游"。

相关推荐
小白学习日记1 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
DOKE2 小时前
VSCode终端:提升命令行使用体验
前端
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081352 小时前
前端之路-了解原型和原型链
前端
永远不打烊2 小时前
librtmp 原生API做直播推流
前端
北极小狐2 小时前
浏览器事件处理机制:从硬件中断到事件驱动
前端
无咎.lsy2 小时前
vue之vuex的使用及举例
前端·javascript·vue.js