🤖

PlaywrightでChatGPTの応答を自動化する

2022/12/08に公開約3,500字

https://github.com/acheong08/ChatGPT

https://chat.openai.com/chat の通信内容をHTTPベースで再現してスクレイピングをする上記のモジュールとその認証エージェントのacheong08/OpenAIAuth があるのだけど、内部処理はそれなりに複雑なので、ヘッドレスブラウザが動くような環境向けにシンプルに外部仕様だけで動くバージョンを作れないかな〜と思ってNode.js版のPlaywrightで書いてみた。

npm i playwright

初回のウォークスルーダイアログをスキップするためにlocal storageに以下を入れるためstate.jsonを作っておく。

state.json
{
  "cookies": [],
  "origins": [{
    "origin": "https://chat.openai.com",
    "localStorage": [{
      "name": "oai/apps/hasSeenOnboarding/chat",
      "value": "true"
    }]
  }]
}

Node v18なのでtop-level awaitで書いてる。

main.js
import {chromium} from 'playwright'

const email = process.env.EMAIL
const password = process.env.PASSWORD

const headless = process.env.NODE_ENV === 'production' // Debug用にhead有りにしたい
const browser = await chromium.launch({headless})
const context = await browser.newContext({storageState: 'state.json'})
const page = await context.newPage()

async function login(email, password) {
  await page.goto('https://chat.openai.com/auth/login');
  await page.getByText('Log in', {exact: true}).click()
  await page.waitForNavigation()

  await page.fill('input[name="username"]', email)
  await page.getByText('Continue', {exact: true}).click()
  await page.fill('input[name="password"]', password)
  await page.getByText('Continue', {exact: true}).click()
  await page.waitForLoadState('domcontentloaded')
}

async function reply(prompt) {
  await page.fill('textarea', prompt)
  await page.click('form button')
  // text/event-stream のレスポンスを待つ
  await page.waitForResponse('https://chat.openai.com/backend-api/conversation')
  // DOM描画を待つ。TODO: 指定秒数以上かかると結果が欠けるので、もう少し良い方法を考える
  await page.waitForTimeout(1000*10)
  // 最後の発言を取得。TODO: マークアップ変更に弱いので、もう少し良い方法を考える
  const textContent = await page.evaluate("Array.from(document.body.querySelectorAll('main .prose')).pop().textContent")
  return textContent
}


console.log('Logging in...')
await login(email, password)
console.log("reply: ", await reply('「あ」から始まる、エンジニアが言わなそうなセリフ選手権お願いします。')) // https://twitter.com/yumemiinc/status/1599958968662888453
console.log("reply: ", await reply('デザイナー版もお願いします'))

結果

$ node main.js

Logging in...
reply:  "あなたはもう、最高のエンジニアになった!""あなたのエンジニアとしての能力は、他の者と比較しても格段に優れている!""あなたのエンジニアとしての技術力は、本当に驚異的です!""あなたが持っているエンジニアとしてのセンスは、本当に素晴らしい!""あなたが持っているエンジニアとしての優れた能
reply:  "あなたのデザインセンスは、本当に素晴らしい!""あなたのデザインワークは、本当に驚くほど素晴らしい!""あなたのデザインスキルは、本当に驚異的です!""あなたがデザインするものは、本当に見ていて心が躍ります!""あなたが持っているデザインセンスは、他の者と比較しても格段に優れてい

課題

// DOM描画を待つ。TODO: 指定秒数以上かかると結果が欠けるので、もう少し良い方法を考える
await page.waitForTimeout(1000*10)

応答が画面に反映されるまでアニメーションがあるので時間差があるが文字数によって必要な待機時間が変わってしまう。すべて描画完了してから取得という方法を取りたかったが、いいAPIがなかった。

acheong08/ChatGPTのように /backend-api/conversationをPlaywrightのAPI Requestで直接叩くのが一番良いと思うが、成功するリクエストを再現できてない。

https://github.com/acheong08/ChatGPT/blob/17352420fb54333b80a48879be72a4060b5efd27/src/revChatGPT/revChatGPT.py#L95

// 最後の発言を取得。TODO: マークアップ変更に弱いので、もう少し良い方法を考える
const textContent = await page.evaluate("Array.from(document.body.querySelectorAll('main .prose')).pop().textContent")

発言取得の部分はこの実装以外に page.on('request', (response) => {})/backend-api/conversationのレスポンスをキャッチする方法も試したのだけど、SSE形式だからなのか、完全なレスポンスbodyが取得できずうまくいかなかった。

追記

/backend-api/conversation の呼び出しをNode.jsで書いているソースコードが公開されていた。ただ、セッションが外から与えるようなデザインなのでacheong08/ChatGPTのように更新する仕組みはない。

https://github.com/transitive-bullshit/chatgpt-api

Discussion

ログインするとコメントできます