mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 21:37:11 +00:00
feat: 暂时不使用chatgpt-api库,拷贝了部分其代码进行修改
This commit is contained in:
parent
b8bd3dfec9
commit
d0dcc50764
5 changed files with 1166 additions and 19 deletions
14
apps/chat.js
14
apps/chat.js
|
|
@ -4,6 +4,7 @@ import _ from 'lodash'
|
||||||
import { Config } from '../config/index.js'
|
import { Config } from '../config/index.js'
|
||||||
import showdown from 'showdown'
|
import showdown from 'showdown'
|
||||||
import mjAPI from 'mathjax-node'
|
import mjAPI from 'mathjax-node'
|
||||||
|
import { ChatGPTPuppeteer } from '../utils/browser.js'
|
||||||
// import puppeteer from '../utils/browser.js'
|
// import puppeteer from '../utils/browser.js'
|
||||||
// import showdownKatex from 'showdown-katex'
|
// import showdownKatex from 'showdown-katex'
|
||||||
const blockWords = '屏蔽词1,屏蔽词2,屏蔽词3'
|
const blockWords = '屏蔽词1,屏蔽词2,屏蔽词3'
|
||||||
|
|
@ -181,11 +182,11 @@ export class chatgpt extends plugin {
|
||||||
// option.debug = true
|
// option.debug = true
|
||||||
option.email = Config.username
|
option.email = Config.username
|
||||||
option.password = Config.password
|
option.password = Config.password
|
||||||
this.chatGPTApi = new ChatGPTAPIBrowser(option)
|
this.chatGPTApi = new ChatGPTPuppeteer(option)
|
||||||
await redis.set('CHATGPT:API_OPTION', JSON.stringify(option))
|
await redis.set('CHATGPT:API_OPTION', JSON.stringify(option))
|
||||||
} else {
|
} else {
|
||||||
let option = JSON.parse(api)
|
let option = JSON.parse(api)
|
||||||
this.chatGPTApi = new ChatGPTAPIBrowser(option)
|
this.chatGPTApi = new ChatGPTPuppeteer(option)
|
||||||
}
|
}
|
||||||
let question = e.msg.trimStart()
|
let question = e.msg.trimStart()
|
||||||
await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 5 })
|
await this.reply('我正在思考如何回复你,请稍等', true, { recallMsg: 5 })
|
||||||
|
|
@ -194,6 +195,7 @@ export class chatgpt extends plugin {
|
||||||
await this.chatGPTApi.init()
|
await this.chatGPTApi.init()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.reply('chatgpt初始化出错:' + e.msg, true)
|
await this.reply('chatgpt初始化出错:' + e.msg, true)
|
||||||
|
await this.chatGPTApi.close()
|
||||||
}
|
}
|
||||||
let previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`)
|
let previousConversation = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`)
|
||||||
let conversation = null
|
let conversation = null
|
||||||
|
|
@ -234,6 +236,7 @@ export class chatgpt extends plugin {
|
||||||
const blockWord = blockWords.split(',').find(word => response.toLowerCase().includes(word.toLowerCase()))
|
const blockWord = blockWords.split(',').find(word => response.toLowerCase().includes(word.toLowerCase()))
|
||||||
if (blockWord) {
|
if (blockWord) {
|
||||||
await this.reply('返回内容存在敏感词,我不想回答你', true)
|
await this.reply('返回内容存在敏感词,我不想回答你', true)
|
||||||
|
await this.chatGPTApi.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||||
|
|
@ -247,18 +250,24 @@ export class chatgpt extends plugin {
|
||||||
if (userSetting.usePicture) {
|
if (userSetting.usePicture) {
|
||||||
let endTokens = ['.', '。', '……', '!', '!', ']', ')', ')', '】', '?', '?', '~', '"', "'"]
|
let endTokens = ['.', '。', '……', '!', '!', ']', ')', ')', '】', '?', '?', '~', '"', "'"]
|
||||||
while (!endTokens.find(token => response.trimEnd().endsWith(token))) {
|
while (!endTokens.find(token => response.trimEnd().endsWith(token))) {
|
||||||
|
// while (!response.trimEnd().endsWith('.') && !response.trimEnd().endsWith('。') && !response.trimEnd().endsWith('……') &&
|
||||||
|
// !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith(']') && !response.trimEnd().endsWith('】')
|
||||||
|
// ) {
|
||||||
await this.reply('内容有点多,我正在奋笔疾书,请再等一会', true, { recallMsg: 5 })
|
await this.reply('内容有点多,我正在奋笔疾书,请再等一会', true, { recallMsg: 5 })
|
||||||
const responseAppend = await this.chatGPTApi.sendMessage('Continue')
|
const responseAppend = await this.chatGPTApi.sendMessage('Continue')
|
||||||
|
// console.log(responseAppend)
|
||||||
// 检索是否有屏蔽词
|
// 检索是否有屏蔽词
|
||||||
const blockWord = blockWords.split(',').find(word => responseAppend.toLowerCase().includes(word.toLowerCase()))
|
const blockWord = blockWords.split(',').find(word => responseAppend.toLowerCase().includes(word.toLowerCase()))
|
||||||
if (blockWord) {
|
if (blockWord) {
|
||||||
await this.reply('返回内容存在敏感词,我不想回答你', true)
|
await this.reply('返回内容存在敏感词,我不想回答你', true)
|
||||||
|
await this.chatGPTApi.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (responseAppend.indexOf('conversation') > -1 || responseAppend.startsWith("I'm sorry")) {
|
if (responseAppend.indexOf('conversation') > -1 || responseAppend.startsWith("I'm sorry")) {
|
||||||
logger.warn('chatgpt might forget what it had said')
|
logger.warn('chatgpt might forget what it had said')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
response = response + responseAppend
|
response = response + responseAppend
|
||||||
}
|
}
|
||||||
// logger.info(response)
|
// logger.info(response)
|
||||||
|
|
@ -275,5 +284,6 @@ export class chatgpt extends plugin {
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
await this.reply(`与OpenAI通信异常,请稍后重试:${e}`, true, { recallMsg: e.isGroup ? 10 : 0 })
|
await this.reply(`与OpenAI通信异常,请稍后重试:${e}`, true, { recallMsg: e.isGroup ? 10 : 0 })
|
||||||
}
|
}
|
||||||
|
await this.chatGPTApi.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,13 @@ export const Config = {
|
||||||
// 改为true后,全局默认以图片形式回复,并自动发出Continue命令补全回答
|
// 改为true后,全局默认以图片形式回复,并自动发出Continue命令补全回答
|
||||||
defaultUsePicture: true,
|
defaultUsePicture: true,
|
||||||
// 每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。单位:秒
|
// 每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。单位:秒
|
||||||
conversationPreserveTime: 600,
|
// conversationPreserveTime: 600,
|
||||||
// UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
// UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
||||||
// headless: false,
|
// 服务器无interface的话只能用true,但是可能遇到验证码就一定要配置下面的2captchaToken了
|
||||||
|
headless: false,
|
||||||
// 为空使用默认puppeteer的chromium,也可以传递自己本机安装的Chrome可执行文件地址,提高通过率
|
// 为空使用默认puppeteer的chromium,也可以传递自己本机安装的Chrome可执行文件地址,提高通过率
|
||||||
// 目前库暂时不支持指定chrome
|
// 目前库暂时不支持指定chrome
|
||||||
// chromePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
chromePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||||
// 可注册2captcha实现跳过验证码,收费服务但很便宜
|
// 可注册2captcha实现跳过验证码,收费服务但很便宜。否则需要手点
|
||||||
'2captchaToken': ''
|
'2captchaToken': ''
|
||||||
}
|
}
|
||||||
|
|
|
||||||
951
utils/browser.js
951
utils/browser.js
|
|
@ -1,7 +1,13 @@
|
||||||
import lodash from 'lodash'
|
import lodash from 'lodash'
|
||||||
import cfg from '../../../lib/config/config.js'
|
|
||||||
import { Config } from '../config/index.js'
|
import { Config } from '../config/index.js'
|
||||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
|
||||||
|
import { getOpenAIAuth } from './openai-auth.js'
|
||||||
|
import delay from 'delay'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import pTimeout, { TimeoutError } from 'p-timeout'
|
||||||
|
import { getBrowser } from 'chatgpt'
|
||||||
|
import { PuppeteerExtraPluginRecaptcha } from 'puppeteer-extra-plugin-recaptcha'
|
||||||
|
const chatUrl = 'https://chat.openai.com/chat'
|
||||||
let puppeteer = {}
|
let puppeteer = {}
|
||||||
|
|
||||||
class Puppeteer {
|
class Puppeteer {
|
||||||
|
|
@ -9,7 +15,7 @@ class Puppeteer {
|
||||||
let args = [
|
let args = [
|
||||||
'--exclude-switches',
|
'--exclude-switches',
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
'enable-automation',
|
'enable-automation'
|
||||||
// '--shm-size=1gb'
|
// '--shm-size=1gb'
|
||||||
]
|
]
|
||||||
if (Config.proxy) {
|
if (Config.proxy) {
|
||||||
|
|
@ -34,6 +40,16 @@ class Puppeteer {
|
||||||
puppeteer = (await import('puppeteer-extra')).default
|
puppeteer = (await import('puppeteer-extra')).default
|
||||||
const pluginStealth = StealthPlugin()
|
const pluginStealth = StealthPlugin()
|
||||||
puppeteer.use(pluginStealth)
|
puppeteer.use(pluginStealth)
|
||||||
|
if (Config['2captchaToken']) {
|
||||||
|
const pluginCaptcha = (await import('puppeteer-extra-plugin-recaptcha')).default
|
||||||
|
puppeteer.use(pluginCaptcha({
|
||||||
|
provider: {
|
||||||
|
id: '2captcha',
|
||||||
|
token: Config['2captchaToken'] // REPLACE THIS WITH YOUR OWN 2CAPTCHA API KEY ⚡
|
||||||
|
},
|
||||||
|
visualFeedback: true
|
||||||
|
}))
|
||||||
|
}
|
||||||
return puppeteer
|
return puppeteer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +83,7 @@ class Puppeteer {
|
||||||
|
|
||||||
/** 监听Chromium实例是否断开 */
|
/** 监听Chromium实例是否断开 */
|
||||||
this.browser.on('disconnected', (e) => {
|
this.browser.on('disconnected', (e) => {
|
||||||
logger.error('Chromium实例关闭或崩溃!')
|
logger.info('Chromium实例关闭或崩溃!')
|
||||||
this.browser = false
|
this.browser = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -75,7 +91,31 @@ class Puppeteer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatGPTPuppeteer extends Puppeteer {
|
export class ChatGPTPuppeteer extends Puppeteer {
|
||||||
|
constructor (opts = {}) {
|
||||||
|
super()
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
markdown = true,
|
||||||
|
debug = false,
|
||||||
|
isGoogleLogin = false,
|
||||||
|
minimize = true,
|
||||||
|
captchaToken,
|
||||||
|
executablePath
|
||||||
|
} = opts
|
||||||
|
|
||||||
|
this._email = email
|
||||||
|
this._password = password
|
||||||
|
|
||||||
|
this._markdown = !!markdown
|
||||||
|
this._debug = !!debug
|
||||||
|
this._isGoogleLogin = !!isGoogleLogin
|
||||||
|
this._minimize = !!minimize
|
||||||
|
this._captchaToken = captchaToken
|
||||||
|
this._executablePath = executablePath
|
||||||
|
}
|
||||||
|
|
||||||
async getBrowser () {
|
async getBrowser () {
|
||||||
if (this.browser) {
|
if (this.browser) {
|
||||||
return this.browser
|
return this.browser
|
||||||
|
|
@ -83,6 +123,909 @@ class ChatGPTPuppeteer extends Puppeteer {
|
||||||
return await this.browserInit()
|
return await this.browserInit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async init () {
|
||||||
|
if (this.inited) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// this.browser = await getBrowser({
|
||||||
|
// captchaToken: this._captchaToken,
|
||||||
|
// executablePath: this._executablePath
|
||||||
|
// })
|
||||||
|
this.browser = await this.getBrowser()
|
||||||
|
this._page =
|
||||||
|
(await this.browser.pages())[0] || (await this.browser.newPage())
|
||||||
|
await maximizePage(this._page)
|
||||||
|
this._page.on('request', this._onRequest.bind(this))
|
||||||
|
this._page.on('response', this._onResponse.bind(this))
|
||||||
|
// bypass cloudflare and login
|
||||||
|
let preCookies = await redis.get('CHATGPT:RAW_COOKIES')
|
||||||
|
if (preCookies) {
|
||||||
|
await this._page.setCookie(...JSON.parse(preCookies))
|
||||||
|
}
|
||||||
|
await this._page.goto(chatUrl, {
|
||||||
|
waitUntil: 'networkidle2'
|
||||||
|
})
|
||||||
|
if (!await this.getIsAuthenticated()) {
|
||||||
|
console.log('需要登录,准备进行自动化登录')
|
||||||
|
await getOpenAIAuth({
|
||||||
|
email: this._email,
|
||||||
|
password: this._password,
|
||||||
|
browser: this.browser,
|
||||||
|
page: this._page,
|
||||||
|
isGoogleLogin: this._isGoogleLogin
|
||||||
|
})
|
||||||
|
console.log('登陆完成')
|
||||||
|
}
|
||||||
|
this.inited = true
|
||||||
|
} catch (err) {
|
||||||
|
if (this.browser) {
|
||||||
|
await this.browser.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.browser = null
|
||||||
|
this._page = null
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this._page.url().replace(/\/$/, '')
|
||||||
|
|
||||||
|
if (url !== chatUrl) {
|
||||||
|
await this._page.goto(chatUrl, {
|
||||||
|
waitUntil: 'networkidle2'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// dismiss welcome modal (and other modals)
|
||||||
|
do {
|
||||||
|
const modalSelector = '[data-headlessui-state="open"]'
|
||||||
|
|
||||||
|
if (!(await this._page.$(modalSelector))) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._page.click(`${modalSelector} button:last-child`)
|
||||||
|
} catch (err) {
|
||||||
|
// "next" button not found in welcome modal
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(300)
|
||||||
|
} while (true)
|
||||||
|
|
||||||
|
if (!await this.getIsAuthenticated()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._minimize) {
|
||||||
|
await minimizePage(this._page)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRequest = (request) => {
|
||||||
|
const url = request.url()
|
||||||
|
if (!isRelevantRequest(url)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = request.method()
|
||||||
|
let body
|
||||||
|
|
||||||
|
if (method === 'POST') {
|
||||||
|
body = request.postData()
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = JSON.parse(body)
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (url.endsWith('/conversation') && typeof body === 'object') {
|
||||||
|
// const conversationBody: types.ConversationJSONBody = body
|
||||||
|
// const conversationId = conversationBody.conversation_id
|
||||||
|
// const parentMessageId = conversationBody.parent_message_id
|
||||||
|
// const messageId = conversationBody.messages?.[0]?.id
|
||||||
|
// const prompt = conversationBody.messages?.[0]?.content?.parts?.[0]
|
||||||
|
|
||||||
|
// // TODO: store this info for the current sendMessage request
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
console.log('\nrequest', {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: request.headers(),
|
||||||
|
body
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onResponse = async (response) => {
|
||||||
|
const request = response.request()
|
||||||
|
|
||||||
|
const url = response.url()
|
||||||
|
if (!isRelevantRequest(url)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status()
|
||||||
|
|
||||||
|
let body
|
||||||
|
try {
|
||||||
|
body = await response.json()
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._debug) {
|
||||||
|
console.log('\nresponse', {
|
||||||
|
url,
|
||||||
|
ok: response.ok(),
|
||||||
|
status,
|
||||||
|
statusText: response.statusText(),
|
||||||
|
headers: response.headers(),
|
||||||
|
body,
|
||||||
|
request: {
|
||||||
|
method: request.method(),
|
||||||
|
headers: request.headers(),
|
||||||
|
body: request.postData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.endsWith('/conversation')) {
|
||||||
|
if (status === 403) {
|
||||||
|
await this.handle403Error()
|
||||||
|
}
|
||||||
|
} else if (url.endsWith('api/auth/session')) {
|
||||||
|
if (status === 403) {
|
||||||
|
await this.handle403Error()
|
||||||
|
} else {
|
||||||
|
const session = body
|
||||||
|
|
||||||
|
if (session?.accessToken) {
|
||||||
|
this._accessToken = session.accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle403Error () {
|
||||||
|
console.log(`ChatGPT "${this._email}" session expired; refreshing...`)
|
||||||
|
try {
|
||||||
|
await maximizePage(this._page)
|
||||||
|
await this._page.reload({
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: 2 * 60 * 1000 // 2 minutes
|
||||||
|
})
|
||||||
|
if (this._minimize) {
|
||||||
|
await minimizePage(this._page)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`ChatGPT "${this._email}" error refreshing session`,
|
||||||
|
err.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIsAuthenticated () {
|
||||||
|
try {
|
||||||
|
const inputBox = await this._getInputBox()
|
||||||
|
return !!inputBox
|
||||||
|
} catch (err) {
|
||||||
|
// can happen when navigating during login
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async getLastMessage(): Promise<string | null> {
|
||||||
|
// const messages = await this.getMessages()
|
||||||
|
|
||||||
|
// if (messages) {
|
||||||
|
// return messages[messages.length - 1]
|
||||||
|
// } else {
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async getPrompts(): Promise<string[]> {
|
||||||
|
// // Get all prompts
|
||||||
|
// const messages = await this._page.$$(
|
||||||
|
// '.text-base:has(.whitespace-pre-wrap):not(:has(button:nth-child(2))) .whitespace-pre-wrap'
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // Prompts are always plaintext
|
||||||
|
// return Promise.all(messages.map((a) => a.evaluate((el) => el.textContent)))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async getMessages(): Promise<string[]> {
|
||||||
|
// // Get all complete messages
|
||||||
|
// // (in-progress messages that are being streamed back don't contain action buttons)
|
||||||
|
// const messages = await this._page.$$(
|
||||||
|
// '.text-base:has(.whitespace-pre-wrap):has(button:nth-child(2)) .whitespace-pre-wrap'
|
||||||
|
// )
|
||||||
|
|
||||||
|
// if (this._markdown) {
|
||||||
|
// const htmlMessages = await Promise.all(
|
||||||
|
// messages.map((a) => a.evaluate((el) => el.innerHTML))
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const markdownMessages = htmlMessages.map((messageHtml) => {
|
||||||
|
// // parse markdown from message HTML
|
||||||
|
// messageHtml = messageHtml
|
||||||
|
// .replaceAll('Copy code</button>', '</button>')
|
||||||
|
// .replace(/Copy code\s*<\/button>/gim, '</button>')
|
||||||
|
|
||||||
|
// return html2md(messageHtml, {
|
||||||
|
// ignoreTags: [
|
||||||
|
// 'button',
|
||||||
|
// 'svg',
|
||||||
|
// 'style',
|
||||||
|
// 'form',
|
||||||
|
// 'noscript',
|
||||||
|
// 'script',
|
||||||
|
// 'meta',
|
||||||
|
// 'head'
|
||||||
|
// ],
|
||||||
|
// skipTags: ['button', 'svg']
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return markdownMessages
|
||||||
|
// } else {
|
||||||
|
// // plaintext
|
||||||
|
// const plaintextMessages = await Promise.all(
|
||||||
|
// messages.map((a) => a.evaluate((el) => el.textContent))
|
||||||
|
// )
|
||||||
|
// return plaintextMessages
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
async sendMessage (
|
||||||
|
message,
|
||||||
|
opts = {}
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
conversationId,
|
||||||
|
parentMessageId = uuidv4(),
|
||||||
|
messageId = uuidv4(),
|
||||||
|
action = 'next',
|
||||||
|
// TODO
|
||||||
|
timeoutMs,
|
||||||
|
// onProgress,
|
||||||
|
onConversationResponse
|
||||||
|
} = opts
|
||||||
|
|
||||||
|
const inputBox = await this._getInputBox()
|
||||||
|
if (!inputBox || !this._accessToken) {
|
||||||
|
console.log(`chatgpt re-authenticating ${this._email}`)
|
||||||
|
let isAuthenticated = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
isAuthenticated = await this.init()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`chatgpt error re-authenticating ${this._email}`,
|
||||||
|
err.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated || !this._accessToken) {
|
||||||
|
const error = new Error('Not signed in')
|
||||||
|
error.statusCode = 401
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = 'https://chat.openai.com/backend-api/conversation'
|
||||||
|
const body = {
|
||||||
|
action,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: messageId,
|
||||||
|
role: 'user',
|
||||||
|
content: {
|
||||||
|
content_type: 'text',
|
||||||
|
parts: [message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
model: 'text-davinci-002-render',
|
||||||
|
parent_message_id: parentMessageId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversationId) {
|
||||||
|
body.conversation_id = conversationId
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('>>> EVALUATE', url, this._accessToken, body)
|
||||||
|
const result = await this._page.evaluate(
|
||||||
|
browserPostEventStream,
|
||||||
|
url,
|
||||||
|
this._accessToken,
|
||||||
|
body,
|
||||||
|
timeoutMs
|
||||||
|
)
|
||||||
|
// console.log('<<< EVALUATE', result)
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
const error = new Error(result.error.message)
|
||||||
|
error.statusCode = result.error.statusCode
|
||||||
|
error.statusText = result.error.statusText
|
||||||
|
|
||||||
|
if (error.statusCode === 403) {
|
||||||
|
await this.handle403Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: support sending partial response events
|
||||||
|
if (onConversationResponse) {
|
||||||
|
onConversationResponse(result.conversationResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.response
|
||||||
|
|
||||||
|
// const lastMessage = await this.getLastMessage()
|
||||||
|
|
||||||
|
// await inputBox.focus()
|
||||||
|
// const paragraphs = message.split('\n')
|
||||||
|
// for (let i = 0; i < paragraphs.length; i++) {
|
||||||
|
// await inputBox.type(paragraphs[i], { delay: 0 })
|
||||||
|
// if (i < paragraphs.length - 1) {
|
||||||
|
// await this._page.keyboard.down('Shift')
|
||||||
|
// await inputBox.press('Enter')
|
||||||
|
// await this._page.keyboard.up('Shift')
|
||||||
|
// } else {
|
||||||
|
// await inputBox.press('Enter')
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const responseP = new Promise<string>(async (resolve, reject) => {
|
||||||
|
// try {
|
||||||
|
// do {
|
||||||
|
// await delay(1000)
|
||||||
|
|
||||||
|
// // TODO: this logic needs some work because we can have repeat messages...
|
||||||
|
// const newLastMessage = await this.getLastMessage()
|
||||||
|
// if (
|
||||||
|
// newLastMessage &&
|
||||||
|
// lastMessage?.toLowerCase() !== newLastMessage?.toLowerCase()
|
||||||
|
// ) {
|
||||||
|
// return resolve(newLastMessage)
|
||||||
|
// }
|
||||||
|
// } while (true)
|
||||||
|
// } catch (err) {
|
||||||
|
// return reject(err)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (timeoutMs) {
|
||||||
|
// return pTimeout(responseP, {
|
||||||
|
// milliseconds: timeoutMs
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// return responseP
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetThread () {
|
||||||
|
try {
|
||||||
|
await this._page.click('nav > a:nth-child(1)')
|
||||||
|
} catch (err) {
|
||||||
|
// ignore for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
await this.browser.close()
|
||||||
|
this._page = null
|
||||||
|
this.browser = null
|
||||||
|
}
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
async _getInputBox () {
|
||||||
|
// [data-id="root"]
|
||||||
|
return this._page.$('textarea')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ChatGPTPuppeteer()
|
export default new ChatGPTPuppeteer()
|
||||||
|
|
||||||
|
export async function minimizePage (page) {
|
||||||
|
const session = await page.target().createCDPSession()
|
||||||
|
const goods = await session.send('Browser.getWindowForTarget')
|
||||||
|
const { windowId } = goods
|
||||||
|
await session.send('Browser.setWindowBounds', {
|
||||||
|
windowId,
|
||||||
|
bounds: { windowState: 'minimized' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function maximizePage (page) {
|
||||||
|
const session = await page.target().createCDPSession()
|
||||||
|
const goods = await session.send('Browser.getWindowForTarget')
|
||||||
|
const { windowId } = goods
|
||||||
|
await session.send('Browser.setWindowBounds', {
|
||||||
|
windowId,
|
||||||
|
bounds: { windowState: 'normal' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRelevantRequest (url) {
|
||||||
|
let pathname
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url)
|
||||||
|
pathname = parsedUrl.pathname
|
||||||
|
url = parsedUrl.toString()
|
||||||
|
} catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.startsWith('https://chat.openai.com')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!pathname.startsWith('/backend-api/') &&
|
||||||
|
!pathname.startsWith('/api/auth/session')
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.endsWith('backend-api/moderations')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is injected into the ChatGPT webapp page using puppeteer. It
|
||||||
|
* has to be fully self-contained, so we copied a few third-party sources and
|
||||||
|
* included them in here.
|
||||||
|
*/
|
||||||
|
export async function browserPostEventStream (
|
||||||
|
url,
|
||||||
|
accessToken,
|
||||||
|
body,
|
||||||
|
timeoutMs
|
||||||
|
) {
|
||||||
|
// Workaround for https://github.com/esbuild-kit/tsx/issues/113
|
||||||
|
globalThis.__name = () => undefined
|
||||||
|
|
||||||
|
const BOM = [239, 187, 191]
|
||||||
|
|
||||||
|
let conversationResponse
|
||||||
|
let conversationId = body?.conversation_id
|
||||||
|
let messageId = body?.messages?.[0]?.id
|
||||||
|
let response = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('browserPostEventStream', url, accessToken, body)
|
||||||
|
|
||||||
|
let abortController = null
|
||||||
|
if (timeoutMs) {
|
||||||
|
abortController = new AbortController()
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: abortController?.signal,
|
||||||
|
headers: {
|
||||||
|
accept: 'text/event-stream',
|
||||||
|
'x-openai-assistant-app-id': '',
|
||||||
|
authorization: `Bearer ${accessToken}`,
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('browserPostEventStream response', res)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: `ChatGPTAPI error ${res.status || res.statusText}`,
|
||||||
|
statusCode: res.status,
|
||||||
|
statusText: res.statusText
|
||||||
|
},
|
||||||
|
response: null,
|
||||||
|
conversationId,
|
||||||
|
messageId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseP = new Promise(
|
||||||
|
async (resolve, reject) => {
|
||||||
|
function onMessage (data) {
|
||||||
|
if (data === '[DONE]') {
|
||||||
|
return resolve({
|
||||||
|
error: null,
|
||||||
|
response,
|
||||||
|
conversationId,
|
||||||
|
messageId,
|
||||||
|
conversationResponse
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const convoResponseEvent =
|
||||||
|
JSON.parse(data)
|
||||||
|
conversationResponse = convoResponseEvent
|
||||||
|
if (convoResponseEvent.conversation_id) {
|
||||||
|
conversationId = convoResponseEvent.conversation_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convoResponseEvent.message?.id) {
|
||||||
|
messageId = convoResponseEvent.message.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const partialResponse =
|
||||||
|
convoResponseEvent.message?.content?.parts?.[0]
|
||||||
|
if (partialResponse) {
|
||||||
|
response = partialResponse
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('fetchSSE onMessage unexpected error', err)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = createParser((event) => {
|
||||||
|
if (event.type === 'event') {
|
||||||
|
onMessage(event.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (const chunk of streamAsyncIterable(res.body)) {
|
||||||
|
const str = new TextDecoder().decode(chunk)
|
||||||
|
parser.feed(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (timeoutMs) {
|
||||||
|
if (abortController) {
|
||||||
|
// This will be called when a timeout occurs in order for us to forcibly
|
||||||
|
// ensure that the underlying HTTP request is aborted.
|
||||||
|
responseP.cancel = () => {
|
||||||
|
abortController.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await pTimeout(responseP, {
|
||||||
|
milliseconds: timeoutMs,
|
||||||
|
message: 'ChatGPT timed out waiting for response'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return await responseP
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errMessageL = err.toString().toLowerCase()
|
||||||
|
|
||||||
|
if (
|
||||||
|
response &&
|
||||||
|
(errMessageL === 'error: typeerror: terminated' ||
|
||||||
|
errMessageL === 'typeerror: terminated')
|
||||||
|
) {
|
||||||
|
// OpenAI sometimes forcefully terminates the socket from their end before
|
||||||
|
// the HTTP request has resolved cleanly. In my testing, these cases tend to
|
||||||
|
// happen when OpenAI has already send the last `response`, so we can ignore
|
||||||
|
// the `fetch` error in this case.
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
response,
|
||||||
|
conversationId,
|
||||||
|
messageId,
|
||||||
|
conversationResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message: err.toString(),
|
||||||
|
statusCode: err.statusCode || err.status || err.response?.statusCode,
|
||||||
|
statusText: err.statusText || err.response?.statusText
|
||||||
|
},
|
||||||
|
response: null,
|
||||||
|
conversationId,
|
||||||
|
messageId,
|
||||||
|
conversationResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function * streamAsyncIterable (stream) {
|
||||||
|
const reader = stream.getReader()
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
yield value
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://github.com/rexxars/eventsource-parser
|
||||||
|
function createParser (onParse) {
|
||||||
|
// Processing state
|
||||||
|
let isFirstChunk
|
||||||
|
let buffer
|
||||||
|
let startingPosition
|
||||||
|
let startingFieldLength
|
||||||
|
|
||||||
|
// Event state
|
||||||
|
let eventId
|
||||||
|
let eventName
|
||||||
|
let data
|
||||||
|
|
||||||
|
reset()
|
||||||
|
return { feed, reset }
|
||||||
|
|
||||||
|
function reset () {
|
||||||
|
isFirstChunk = true
|
||||||
|
buffer = ''
|
||||||
|
startingPosition = 0
|
||||||
|
startingFieldLength = -1
|
||||||
|
|
||||||
|
eventId = undefined
|
||||||
|
eventName = undefined
|
||||||
|
data = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function feed (chunk) {
|
||||||
|
buffer = buffer ? buffer + chunk : chunk
|
||||||
|
|
||||||
|
// Strip any UTF8 byte order mark (BOM) at the start of the stream.
|
||||||
|
// Note that we do not strip any non - UTF8 BOM, as eventsource streams are
|
||||||
|
// always decoded as UTF8 as per the specification.
|
||||||
|
if (isFirstChunk && hasBom(buffer)) {
|
||||||
|
buffer = buffer.slice(BOM.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
isFirstChunk = false
|
||||||
|
|
||||||
|
// Set up chunk-specific processing state
|
||||||
|
const length = buffer.length
|
||||||
|
let position = 0
|
||||||
|
let discardTrailingNewline = false
|
||||||
|
|
||||||
|
// Read the current buffer byte by byte
|
||||||
|
while (position < length) {
|
||||||
|
// EventSource allows for carriage return + line feed, which means we
|
||||||
|
// need to ignore a linefeed character if the previous character was a
|
||||||
|
// carriage return
|
||||||
|
// @todo refactor to reduce nesting, consider checking previous byte?
|
||||||
|
// @todo but consider multiple chunks etc
|
||||||
|
if (discardTrailingNewline) {
|
||||||
|
if (buffer[position] === '\n') {
|
||||||
|
++position
|
||||||
|
}
|
||||||
|
discardTrailingNewline = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let lineLength = -1
|
||||||
|
let fieldLength = startingFieldLength
|
||||||
|
let character
|
||||||
|
|
||||||
|
for (
|
||||||
|
let index = startingPosition;
|
||||||
|
lineLength < 0 && index < length;
|
||||||
|
++index
|
||||||
|
) {
|
||||||
|
character = buffer[index]
|
||||||
|
if (character === ':' && fieldLength < 0) {
|
||||||
|
fieldLength = index - position
|
||||||
|
} else if (character === '\r') {
|
||||||
|
discardTrailingNewline = true
|
||||||
|
lineLength = index - position
|
||||||
|
} else if (character === '\n') {
|
||||||
|
lineLength = index - position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineLength < 0) {
|
||||||
|
startingPosition = length - position
|
||||||
|
startingFieldLength = fieldLength
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
startingPosition = 0
|
||||||
|
startingFieldLength = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
parseEventStreamLine(buffer, position, fieldLength, lineLength)
|
||||||
|
|
||||||
|
position += lineLength + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position === length) {
|
||||||
|
// If we consumed the entire buffer to read the event, reset the buffer
|
||||||
|
buffer = ''
|
||||||
|
} else if (position > 0) {
|
||||||
|
// If there are bytes left to process, set the buffer to the unprocessed
|
||||||
|
// portion of the buffer only
|
||||||
|
buffer = buffer.slice(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEventStreamLine (
|
||||||
|
lineBuffer,
|
||||||
|
index,
|
||||||
|
fieldLength,
|
||||||
|
lineLength
|
||||||
|
) {
|
||||||
|
if (lineLength === 0) {
|
||||||
|
// We reached the last line of this event
|
||||||
|
if (data.length > 0) {
|
||||||
|
onParse({
|
||||||
|
type: 'event',
|
||||||
|
id: eventId,
|
||||||
|
event: eventName || undefined,
|
||||||
|
data: data.slice(0, -1) // remove trailing newline
|
||||||
|
})
|
||||||
|
|
||||||
|
data = ''
|
||||||
|
eventId = undefined
|
||||||
|
}
|
||||||
|
eventName = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const noValue = fieldLength < 0
|
||||||
|
const field = lineBuffer.slice(
|
||||||
|
index,
|
||||||
|
index + (noValue ? lineLength : fieldLength)
|
||||||
|
)
|
||||||
|
let step = 0
|
||||||
|
|
||||||
|
if (noValue) {
|
||||||
|
step = lineLength
|
||||||
|
} else if (lineBuffer[index + fieldLength + 1] === ' ') {
|
||||||
|
step = fieldLength + 2
|
||||||
|
} else {
|
||||||
|
step = fieldLength + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = index + step
|
||||||
|
const valueLength = lineLength - step
|
||||||
|
const value = lineBuffer
|
||||||
|
.slice(position, position + valueLength)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
if (field === 'data') {
|
||||||
|
data += value ? `${value}\n` : '\n'
|
||||||
|
} else if (field === 'event') {
|
||||||
|
eventName = value
|
||||||
|
} else if (field === 'id' && !value.includes('\u0000')) {
|
||||||
|
eventId = value
|
||||||
|
} else if (field === 'retry') {
|
||||||
|
const retry = parseInt(value, 10)
|
||||||
|
if (!Number.isNaN(retry)) {
|
||||||
|
onParse({ type: 'reconnect-interval', value: retry })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hasBom (buffer) {
|
||||||
|
return BOM.every(
|
||||||
|
(charCode, index) => buffer.charCodeAt(index) === charCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
TODO: Remove AbortError and just throw DOMException when targeting Node 18.
|
||||||
|
*/
|
||||||
|
function getDOMException (errorMessage) {
|
||||||
|
return globalThis.DOMException === undefined
|
||||||
|
? new Error(errorMessage)
|
||||||
|
: new DOMException(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18.
|
||||||
|
*/
|
||||||
|
function getAbortedReason (signal) {
|
||||||
|
const reason =
|
||||||
|
signal.reason === undefined
|
||||||
|
? getDOMException('This operation was aborted.')
|
||||||
|
: signal.reason
|
||||||
|
|
||||||
|
return reason instanceof Error ? reason : getDOMException(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://github.com/sindresorhus/p-timeout
|
||||||
|
function pTimeout (
|
||||||
|
promise,
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
milliseconds,
|
||||||
|
fallback,
|
||||||
|
message,
|
||||||
|
customTimers = { setTimeout, clearTimeout }
|
||||||
|
} = options
|
||||||
|
|
||||||
|
let timer
|
||||||
|
|
||||||
|
const cancelablePromise = new Promise((resolve, reject) => {
|
||||||
|
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (milliseconds === Number.POSITIVE_INFINITY) {
|
||||||
|
resolve(promise)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.signal) {
|
||||||
|
const { signal } = options
|
||||||
|
if (signal.aborted) {
|
||||||
|
reject(getAbortedReason(signal))
|
||||||
|
}
|
||||||
|
|
||||||
|
signal.addEventListener('abort', () => {
|
||||||
|
reject(getAbortedReason(signal))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = customTimers.setTimeout.call(
|
||||||
|
undefined,
|
||||||
|
() => {
|
||||||
|
if (fallback) {
|
||||||
|
try {
|
||||||
|
resolve(fallback())
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
typeof message === 'string'
|
||||||
|
? message
|
||||||
|
: `Promise timed out after ${milliseconds} milliseconds`
|
||||||
|
const timeoutError =
|
||||||
|
message instanceof Error ? message : new TimeoutError(errorMessage)
|
||||||
|
|
||||||
|
if (typeof promise.cancel === 'function') {
|
||||||
|
promise.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(timeoutError)
|
||||||
|
},
|
||||||
|
milliseconds
|
||||||
|
)
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
resolve(await promise)
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
} finally {
|
||||||
|
customTimers.clearTimeout.call(undefined, timer)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelablePromise.clear = () => {
|
||||||
|
customTimers.clearTimeout.call(undefined, timer)
|
||||||
|
timer = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancelablePromise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
202
utils/openai-auth.js
Normal file
202
utils/openai-auth.js
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import { Config } from '../config/index.js'
|
||||||
|
import delay from 'delay'
|
||||||
|
|
||||||
|
let hasRecaptchaPlugin = !!Config['2captchaToken']
|
||||||
|
|
||||||
|
export async function getOpenAIAuth (opt) {
|
||||||
|
let {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
browser,
|
||||||
|
page,
|
||||||
|
timeoutMs = 2 * 60 * 1000,
|
||||||
|
isGoogleLogin = false,
|
||||||
|
captchaToken = Config['2captchaToken'],
|
||||||
|
executablePath = Config.chromePath
|
||||||
|
} = opt
|
||||||
|
const origBrowser = browser
|
||||||
|
const origPage = page
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userAgent = await browser.userAgent()
|
||||||
|
if (!page) {
|
||||||
|
page = (await browser.pages())[0] || (await browser.newPage())
|
||||||
|
page.setDefaultTimeout(timeoutMs)
|
||||||
|
}
|
||||||
|
await page.goto('https://chat.openai.com/auth/login', {
|
||||||
|
waitUntil: 'networkidle2'
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOTE: this is where you may encounter a CAPTCHA
|
||||||
|
if (hasRecaptchaPlugin) {
|
||||||
|
await page.solveRecaptchas()
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkForChatGPTAtCapacity(page)
|
||||||
|
|
||||||
|
// once we get to this point, the Cloudflare cookies should be available
|
||||||
|
|
||||||
|
// login as well (optional)
|
||||||
|
if (email && password) {
|
||||||
|
await waitForConditionOrAtCapacity(page, () =>
|
||||||
|
page.waitForSelector('#__next .btn-primary', { timeout: timeoutMs })
|
||||||
|
)
|
||||||
|
await delay(500)
|
||||||
|
|
||||||
|
// click login button and wait for navigation to finish
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForNavigation({
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: timeoutMs
|
||||||
|
}),
|
||||||
|
|
||||||
|
page.click('#__next .btn-primary')
|
||||||
|
])
|
||||||
|
|
||||||
|
await checkForChatGPTAtCapacity(page)
|
||||||
|
|
||||||
|
let submitP
|
||||||
|
|
||||||
|
if (isGoogleLogin) {
|
||||||
|
await page.click('button[data-provider="google"]')
|
||||||
|
await page.waitForSelector('input[type="email"]')
|
||||||
|
await page.type('input[type="email"]', email, { delay: 10 })
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForNavigation(),
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
])
|
||||||
|
await page.waitForSelector('input[type="password"]', { visible: true })
|
||||||
|
await page.type('input[type="password"]', password, { delay: 10 })
|
||||||
|
submitP = () => page.keyboard.press('Enter')
|
||||||
|
} else {
|
||||||
|
await page.waitForSelector('#username')
|
||||||
|
await page.type('#username', email, { delay: 20 })
|
||||||
|
await delay(100)
|
||||||
|
|
||||||
|
if (hasRecaptchaPlugin) {
|
||||||
|
// console.log('solveRecaptchas()')
|
||||||
|
const res = await page.solveRecaptchas()
|
||||||
|
// console.log('solveRecaptchas result', res)
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
await page.waitForSelector('#password', { timeout: timeoutMs })
|
||||||
|
await page.type('#password', password, { delay: 10 })
|
||||||
|
submitP = () => page.click('button[type="submit"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
waitForConditionOrAtCapacity(page, () =>
|
||||||
|
page.waitForNavigation({
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: timeoutMs
|
||||||
|
})
|
||||||
|
),
|
||||||
|
submitP()
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
await delay(2000)
|
||||||
|
await checkForChatGPTAtCapacity(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCookies = await page.cookies()
|
||||||
|
await redis.set('CHATGPT:RAW_COOKIES', JSON.stringify(pageCookies))
|
||||||
|
const cookies = pageCookies.reduce(
|
||||||
|
(map, cookie) => ({ ...map, [cookie.name]: cookie }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
const authInfo = {
|
||||||
|
userAgent,
|
||||||
|
clearanceToken: cookies.cf_clearance?.value,
|
||||||
|
sessionToken: cookies['__Secure-next-auth.session-token']?.value,
|
||||||
|
cookies
|
||||||
|
}
|
||||||
|
|
||||||
|
return authInfo
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
if (origBrowser) {
|
||||||
|
if (page && page !== origPage) {
|
||||||
|
await page.close()
|
||||||
|
}
|
||||||
|
} else if (browser) {
|
||||||
|
await browser.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
page = null
|
||||||
|
browser = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForChatGPTAtCapacity (page) {
|
||||||
|
// console.log('checkForChatGPTAtCapacity', page.url())
|
||||||
|
let res
|
||||||
|
|
||||||
|
try {
|
||||||
|
res = await page.$x("//div[contains(., 'ChatGPT is at capacity')]")
|
||||||
|
// console.log('capacity1', els)
|
||||||
|
// if (els?.length) {
|
||||||
|
// res = await Promise.all(
|
||||||
|
// els.map((a) => a.evaluate((el) => el.textContent))
|
||||||
|
// )
|
||||||
|
// console.log('capacity2', res)
|
||||||
|
// }
|
||||||
|
} catch (err) {
|
||||||
|
// ignore errors likely due to navigation
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res?.length) {
|
||||||
|
const error = new Error('ChatGPT is at capacity')
|
||||||
|
error.statusCode = 503
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForConditionOrAtCapacity (
|
||||||
|
page,
|
||||||
|
condition,
|
||||||
|
opts = {}
|
||||||
|
) {
|
||||||
|
const { pollingIntervalMs = 500 } = opts
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let resolved = false
|
||||||
|
|
||||||
|
async function waitForCapacityText () {
|
||||||
|
if (resolved) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkForChatGPTAtCapacity(page)
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
setTimeout(waitForCapacityText, pollingIntervalMs)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
condition()
|
||||||
|
.then(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(waitForCapacityText, pollingIntervalMs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断一段markdown文档中是否包含代码
|
|
||||||
* @param text
|
|
||||||
*/
|
|
||||||
export function codeExists (text = '') {
|
|
||||||
let regex = /^[\s\S]*\$.*\$[\s\S]*/
|
|
||||||
return regex.test(text)
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue