feat: support browser-based solution. update readme

This commit is contained in:
ikechan8370 2023-02-11 23:17:38 +08:00
parent e59ed0c048
commit 6fdcbf12e2
11 changed files with 1557 additions and 67 deletions

View file

@ -1,29 +1,40 @@
# 云崽qq机器人的chatgpt插件
* 支持单人连续对话Conversation
* 目前使用 GPT-3 API尽可能逼近ChatGPT体验支持模型参数调整 ~~使用`text-chat-davinci-002-sh-alpha-aoruigiofdj83`模型原汁原味ChatGPT体验(只存活了五分钟)~~
* API模式下使用 GPT-3 API及相关模型配置尽可能逼近ChatGPT体验支持自定义部分模型参数 ~~使用`text-chat-davinci-002-sh-alpha-aoruigiofdj83`模型原汁原味ChatGPT体验(20230211只存活了五分钟)~~
* 支持问答图片截图
* 仅需OpenAI Api Key开箱即用
* 提供基于浏览器的解决方案作为备选,有条件且希望得到更好回答质量可以选择使用浏览器模式。
## 版本要求
Node.js >= 18 / Node.js >= 14(with node-fetch)
## 安装
首先判断自己需要使用哪种模式本插件支持API和浏览器两种模式。也可以选择**我全都要**通过qq发送命令`#chatgpt切换浏览器/API`实时切换。对于轻量用户可以先使用API模式有较高要求再转为使用浏览器模式。
> API模式和浏览器模式如何选择
>
> * API模式会调用OpenAI官方提供的GPT-3 LLM API只需要提供API Key。一般情况下该种方式响应速度更快可配置项多且不会像chatGPT官网一样总出现不可用的现象但其聊天效果明显较官网差。但注意GPT-3的API调用是收费的新用户有18美元试用金可用于支付价格为`$0.0200/1K tokens`.(问题和回答加起来算token)
> * 浏览器模式通过在本地启动Chrome等浏览器模拟用户访问ChatGPT网站使得获得和官方一模一样的回复质量。缺点是本方法对环境要求较高需要提供桌面环境和一个可用的代理能够访问ChatGPT的IP地址且响应速度不如API而且高峰期容易无法使用。
1. 进入 Yunzai根目录
2. 检查 Node.js 版本,并根据对应的 Node.js 版本选择安装教程。
```
node -v
```
---
### Node.js >= 18
1. 进入 Yunzai根目录
2. 安装依赖
```
pnpm install -w undici chatgpt showdown mathjax-node delay uuid remark strip-markdown
pnpm install -w undici chatgpt showdown mathjax-node delay uuid remark strip-markdown random puppeteer-extra-plugin-recaptcha puppeteer-extra puppeteer-extra-plugin-stealth
```
**chatgpt的版本号注意要大于4.0.0**
**若使用API模式chatgpt的版本号注意要大于4.2.0**
若不使用浏览器模式,可以不安装`random puppeteer-extra-plugin-recaptcha puppeteer-extra puppeteer-extra-plugin-stealth`这几个
3. 克隆项目
```
@ -31,7 +42,7 @@ git clone https://github.com/ikechan8370/chatgpt-plugin.git ./plugins/chatgpt-pl
```
4. 修改配置
编辑`plugins/chatgpt-plugin/config/index.js`文件主要修改其中的`apiKey`
编辑`plugins/chatgpt-plugin/config/index.js`文件,根据其中的注释修改必要配置项。
---
@ -44,7 +55,9 @@ git clone https://github.com/ikechan8370/chatgpt-plugin.git ./plugins/chatgpt-pl
```
pnpm install -w undici chatgpt showdown mathjax-node delay uuid remark strip-markdown node-fetch
```
**chatgpt的版本号注意要大于4.0.0**
**若使用API模式chatgpt的版本号注意要大于4.2.0**
若不使用浏览器模式,可以不安装`random puppeteer-extra-plugin-recaptcha puppeteer-extra puppeteer-extra-plugin-stealth`这几个
3. 克隆项目
```
@ -63,7 +76,7 @@ import fetch from 'node-fetch';
globalThis.fetch = fetch;
```
再编辑`Yunzai根目录/plugins/chatgpt-plugin/config/index.js`文件,主要修改其中的`apiKey`
再编辑`Yunzai根目录/plugins/chatgpt-plugin/config/index.js`文件,根据其中的注释修改必要配置项。
---
@ -92,10 +105,9 @@ globalThis.fetch = fetch;
## TODO
* 更灵活的Conversation管理
* 恢复网页版支持 (Browser Based Solution)
* 支持Bing版本
## 关于openai token获取
## 关于openai账号
1. 注册openai账号
进入https://chat.openai.com/ 选择signup注册。目前openai不对包括俄罗斯、乌克兰、伊朗、中国等国家和地区提供服务所以自行寻找办法使用其他国家和地区的ip登录。此外注册可能需要验证所在国家和地区的手机号码如果没有国外手机号可以试试解码网站收费的推荐https://sms-activate.org/。
2. 获取API key
@ -106,13 +118,25 @@ globalThis.fetch = fetch;
## 其他
OpenAI 即将开放其官方ChatGPT API请等待此部分内容更新。
### 关于未来更新
> 该api响应速度可能由于模型本身及网络原因不会太快请勿频繁重发。因网络问题和模型响应速度问题可能出现500、503、404等各种异常状态码此时等待官方恢复即可。实测复杂的中文对话更容易触发503错误超时。如出现429则意味着超出了免费账户调用频率只能暂时停用放置一段时间再继续使用。
OpenAI 即将开放其官方ChatGPT API且微软必应也公开发布了基于ChatGPT的问答搜索能够为实现更好、更快的聊天机器人提供更多途径。
### 常见问题
1. 如果在linux系统上发现图片模式下emoj无法正常显示可以搜索安装支持emoj的字体如Ubuntu可以使用`sudo apt install fonts-noto-color-emoji`
2. linux云服务器可以安装窗口管理器和vnc创建并访问虚拟桌面环境。
> 以ubuntu为例给出一个可行的方案
>
> ~~openai目前开放chatgpt模型的免费试用在此期间本项目应该都可用后续如果openai调整其收费策略到时候视情况进行调整。~~ GPT-3的API调用是收费的新用户有18美元试用金可用于支付价格为`$0.0200/1K tokens`,问题和回答加起来算。
> 1. 安装xvfb和fluxbox
> `sudo apt-get install x11vnc xvfb fluxbox`
> 2. 启动桌面环境。建议用tmux或screen等使其能够后台运行。注意本命令使用默认5900端口和无密码注意通过防火墙等保护。
> `x11vnc -create -env FD_PROG=/usr/bin/fluxbox -env X11VNC_FINDDISPLAY_ALWAYS_FAILS=1 -env X11VNC_CREATE_GEOM=${1:-1024x768x16} -nopw -forever`
> 3. 使用vnc客户端连接至云桌面右键Applications > Shells > Bash打开终端然后进入Yunzai目录下运行node app即可。
>
> 如果在linux系统上发现emoj无法正常显示可以搜索安装支持emoj的字体如Ubuntu可以使用`sudo apt install fonts-noto-color-emoji`
> 实测该方案资源占用低运行稳定基本1核2G的轻量云服务器就足够了。
## 感谢
* https://github.com/transitive-bullshit/chatgpt-api

View file

@ -7,6 +7,7 @@ import { uuid } from 'oicq/lib/common.js'
import delay from 'delay'
import { ChatGPTAPI } from 'chatgpt'
import { getMessageById, tryTimes, upsertMessage } from '../utils/common.js'
import { ChatGPTPuppeteer } from '../utils/browser.js'
// import puppeteer from '../utils/browser.js'
// import showdownKatex from 'showdown-katex'
const blockWords = Config.blockWords
@ -78,11 +79,23 @@ export class chatgpt extends plugin {
},
{
reg: '#清空(chat)?队列',
fnc: 'emptyQueue'
fnc: 'emptyQueue',
permission: 'master'
},
{
reg: '#移出(chat)?队列首位',
fnc: 'removeQueueFirst'
fnc: 'removeQueueFirst',
permission: 'master'
},
{
reg: '#chatgpt切换浏览器',
fnc: 'useBrowserBasedSolution',
permission: 'master'
},
{
reg: '#chatgpt切换[(api)|(API)]',
fnc: 'useOpenAIAPIBasedSolution',
permission: 'master'
}
]
})
@ -194,19 +207,6 @@ export class chatgpt extends plugin {
return false
}
}
let completionParams = {}
if (Config.model) {
completionParams.model = Config.model
}
this.chatGPTApi = new ChatGPTAPI({
apiKey: Config.apiKey,
debug: false,
upsertMessage,
getMessageById,
completionParams,
assistantLabel: Config.assistantLabel
})
let randomId = uuid()
// 队列队尾插入,开始排队
@ -258,16 +258,17 @@ export class chatgpt extends plugin {
}
try {
let chatMessage = await this.sendMessage(prompt, conversation, this.chatGPTApi)
let chatMessage = await this.sendMessage(prompt, conversation)
previousConversation.conversation = {
conversationId: chatMessage.conversationId,
parentMessageId: chatMessage.id
}
console.log(chatMessage)
let response = chatMessage?.text
previousConversation.num = previousConversation.num + 1
await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), CONVERSATION_PRESERVE_TIME > 0 ? { EX: CONVERSATION_PRESERVE_TIME } : {})
// 检索是否有屏蔽词
const blockWord = blockWords.split(',').find(word => response.toLowerCase().includes(word.toLowerCase()))
const blockWord = blockWords.find(word => response.toLowerCase().includes(word.toLowerCase()))
if (blockWord) {
await this.reply('返回内容存在敏感词,我不想回答你', true)
return false
@ -289,7 +290,7 @@ export class chatgpt extends plugin {
// !response.trimEnd().endsWith('') && !response.trimEnd().endsWith('!') && !response.trimEnd().endsWith(']') && !response.trimEnd().endsWith('】')
// ) {
await this.reply('内容有点多,我正在奋笔疾书,请再等一会', true, { recallMsg: 5 })
let responseAppend = await this.sendMessage('Continue', conversation, this.chatGPTApi)
let responseAppend = await this.sendMessage('Continue', conversation)
previousConversation.conversation = {
conversationId: responseAppend.conversationId,
parentMessageId: responseAppend.id
@ -298,7 +299,7 @@ export class chatgpt extends plugin {
await redis.set(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`, JSON.stringify(previousConversation), CONVERSATION_PRESERVE_TIME > 0 ? { EX: CONVERSATION_PRESERVE_TIME } : {})
// console.log(responseAppend)
// 检索是否有屏蔽词
const blockWord = blockWords.split(',').find(word => responseAppendText.toLowerCase().includes(word.toLowerCase()))
const blockWord = blockWords.find(word => responseAppendText.toLowerCase().includes(word.toLowerCase()))
if (blockWord) {
await this.reply('返回内容存在敏感词,我不想回答你', true)
return
@ -330,7 +331,24 @@ export class chatgpt extends plugin {
}
}
async sendMessage (prompt, conversation, api) {
async sendMessage (prompt, conversation) {
const use = await redis.get('CHATGPT:USE')
console.log(use)
if (use === 'browser') {
return await this.chatgptBrowserBased(prompt, conversation)
} else {
let completionParams = {}
if (Config.model) {
completionParams.model = Config.model
}
this.chatGPTApi = new ChatGPTAPI({
apiKey: Config.apiKey,
debug: false,
upsertMessage,
getMessageById,
completionParams,
assistantLabel: Config.assistantLabel
})
const currentDate = new Date().toISOString().split('T')[0]
let promptPrefix = `You are ${Config.assistantLabel}, a large language model trained by OpenAI. ${Config.promptPrefixOverride || defaultPropmtPrefix}
Current date: ${currentDate}`
@ -341,7 +359,18 @@ export class chatgpt extends plugin {
if (conversation) {
option = Object.assign(option, conversation)
}
return await tryTimes(async () => await api.sendMessage(prompt, option), 5)
return await tryTimes(async () => await this.chatGPTApi.sendMessage(prompt, option), 5)
}
}
async useBrowserBasedSolution (e) {
await redis.set('CHATGPT:USE', 'browser')
await this.reply('已切换到基于浏览器的解决方案')
}
async useOpenAIAPIBasedSolution (e) {
await redis.set('CHATGPT:USE', 'api')
await this.reply('已切换到基于OpenAI API的解决方案')
}
async emptyQueue (e) {
@ -357,4 +386,28 @@ export class chatgpt extends plugin {
await this.reply('已移出等待队列首位: ' + uid)
}
}
/**
* #chatgpt
* @param prompt 问题
* @param conversation 对话
*/
async chatgptBrowserBased (prompt, conversation) {
let option = { markdown: true }
if (Config['2captchaToken']) {
option.captchaToken = Config['2captchaToken']
}
// option.debug = true
option.email = Config.username
option.password = Config.password
this.chatGPTApi = new ChatGPTPuppeteer(option)
logger.info(`chatgpt prompt: ${prompt}`)
let sendMessageOption = {
timeoutMs: 120000
}
if (conversation) {
sendMessageOption = Object.assign(sendMessageOption, conversation)
}
return await this.chatGPTApi.sendMessage(prompt, sendMessageOption)
}
}

View file

@ -46,19 +46,24 @@ let helpData = [
desc: '结束该用户当前对话,下次开启对话机器人将遗忘掉本次对话内容。'
},
{
icon: 'destroy',
icon: 'queue',
title: '#清空chat队列',
desc: '清空当前对话等待队列。仅建议前方卡死时使用。'
},
{
icon: 'destroy-other',
icon: 'queue',
title: '#移出chat队列首位',
desc: '移出当前对话等待队列中的首位。若前方对话卡死可使用本命令。'
},
{
icon: 'destroy-other',
icon: 'confirm',
title: '#chatgpt开启/关闭问题确认',
desc: '开启或关闭机器人收到消息后的确认回复消息。私聊无效。'
desc: '开启或关闭机器人收到消息后的确认回复消息。'
},
{
icon: 'switch',
title: '#chatgpt切换浏览器/API',
desc: '切换使用的后端为浏览器或OpenAI API'
},
{
icon: 'help',

View file

@ -10,11 +10,13 @@ export class ChatgptManagement extends plugin {
rule: [
{
reg: '#chatgpt开启(问题)?(回复)?确认',
fnc: 'turnOnConfirm'
fnc: 'turnOnConfirm',
permission: 'master'
},
{
reg: '#chatgpt关闭(问题)?(回复)?确认',
fnc: 'turnOffConfirm'
fnc: 'turnOffConfirm',
permission: 'master'
}
]
})

View file

@ -1,25 +1,45 @@
// 例如http://127.0.0.1:7890
const PROXY = ''
const API_KEY = ''
export const Config = {
// 模型名称。如无特殊需求保持默认即可会使用chatgpt-api库提供的当前可用的最适合的默认值。保底可用的是 text-davinci-003。当发现新的可用的chatGPT模型会更新这里的值
// 20230211 text-chat-davinci-002-sh-alpha-aoruigiofdj83 中午存活了几分钟
model: '',
// 如果回答包括屏蔽词,就不返回。例如:'屏蔽词1,屏蔽词2,屏蔽词3'
blockWords: '',
apiKey: API_KEY,
// 暂时不支持proxy
proxy: PROXY,
// 改为true后全局默认以图片形式回复并自动发出Continue命令补全回答
// ***********************************************************************************************************************************
// 通用配置 *
// ***********************************************************************************************************************************
// 如果回答包括屏蔽词,就不返回。
blockWords: ['屏蔽词1', '屏蔽词b'],
// 改为true后全局默认以图片形式回复并自动发出Continue命令补全回答。长回复可能会有bug。
defaultUsePicture: false,
// 每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。单位:秒
conversationPreserveTime: 0,
// 触发方式 可选值at 或 prefix 。at模式下只有at机器人才会回复。prefix模式下不需要at但需要添加前缀#chat
toggleMode: 'prefix',
// 默认完整值:`You are ${this._assistantLabel}, a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. dont be verbose). It is very important that you answer as concisely as possible, so please remember this. If you are generating a list, do not have too many items. Keep the number of items short. Current date: ${currentDate}\n\n
// ***********************************************************************************************************************************
// 以下为API方式(默认)的配置 *
// ***********************************************************************************************************************************
apiKey: API_KEY,
// 模型名称选填。如无特殊需求保持默认即可会使用chatgpt-api库提供的当前可用的最适合的默认值。保底可用的是 text-davinci-003。当发现新的可用的chatGPT模型会更新这里的值
// 20230211 text-chat-davinci-002-sh-alpha-aoruigiofdj83 中午存活了几分钟
model: '',
// 给模型的暗示promt。选填。默认完整值`You are ${this._assistantLabel}, a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. dont be verbose). It is very important that you answer as concisely as possible, so please remember this. If you are generating a list, do not have too many items. Keep the number of items short. Current date: ${currentDate}\n\n
// 此项配置会覆盖掉中间部分。保持为空将使用网友从对话中推测出的指令。
// 你可以在这里写入你希望AI回答的风格比如希望优先回答中文回答长一点等
promptPrefixOverride: 'Your answer shouldn\'t be too verbose. If you are generating a list, do not have too many items. Keep the number of items short. Prefer to answer in Chinese.',
// AI认为的自己的名字当你问他你是谁是他会回答这里的名字。
assistantLabel: 'ChatGPT'
assistantLabel: 'ChatGPT',
// ***********************************************************************************************************************************
// 以下为浏览器方式的配置 *
// ***********************************************************************************************************************************
username: '',
password: '',
// UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
// 服务器无interface的话只能用true但是可能遇到验证码就一定要配置下面的2captchaToken了
// true时使用无头模式无界面的服务器可以为true但遇到验证码时可能无法使用。(实测很容易卡住,几乎不可用)
headless: false,
// 为空使用默认puppeteer的chromium也可以传递自己本机安装的Chrome可执行文件地址提高通过率。windows可以是C:\\Program Files\\Google\\Chrome\\Application\\chrome.exelinux通过which查找路径
chromePath: '',
// 可注册2captcha实现跳过验证码收费服务但很便宜。否则可能会遇到验证码而卡住。
'2captchaToken': '',
// http或socks5代理
proxy: PROXY
}

View file

@ -13,13 +13,13 @@
body {
font-family: sans-serif;
font-size: 16px;
width: 530px;
width: 630px;
color: #1e1f20;
transform: scale(1.5);
transform-origin: 0 0;
}
.container {
width: 530px;
width: 630px;
padding: 20px 15px 10px 15px;
background-color: #f5f6fb;
}
@ -98,7 +98,7 @@ body {
}
.list .item {
width: 235px;
width: 285px;
display: flex;
align-items: center;
background: #f1f1f1;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

1107
utils/browser.js Normal file

File diff suppressed because it is too large Load diff

279
utils/openai-auth.js Normal file
View file

@ -0,0 +1,279 @@
import { Config } from '../config/index.js'
import delay from 'delay'
import random from 'random'
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'
})
logger.mark('chatgpt checkForChatGPTAtCapacity')
await checkForChatGPTAtCapacity(page)
// NOTE: this is where you may encounter a CAPTCHA
if (hasRecaptchaPlugin) {
logger.mark('RecaptchaPlugin key exists, try to solve recaptchas')
await page.solveRecaptchas()
}
logger.mark('chatgpt checkForChatGPTAtCapacity again')
await checkForChatGPTAtCapacity(page)
// once we get to this point, the Cloudflare cookies should be available
// login as well (optional)
if (email && password) {
let retry = 3
while (retry > 0) {
try {
await waitForConditionOrAtCapacity(page, () =>
page.waitForSelector('#__next .btn-primary', { timeout: 2000 })
)
} catch (e) {
await checkForChatGPTAtCapacity(page)
}
retry--
}
await waitForConditionOrAtCapacity(page, () =>
page.waitForSelector('#__next .btn-primary', { timeout: 2000 })
)
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
}
logger.info('chatgpt登录成功')
return authInfo
} catch (err) {
throw err
} finally {
await page.screenshot({
type: 'png',
path: './error.png'
})
if (origBrowser) {
if (page && page !== origPage) {
await page.close()
}
} else if (browser) {
await browser.close()
}
page = null
browser = null
}
}
async function checkForChatGPTAtCapacity (page, opts = {}) {
const {
timeoutMs = 2 * 60 * 1000, // 2 minutes
pollingIntervalMs = 3000,
retries = 10
} = opts
// console.log('checkForChatGPTAtCapacity', page.url())
let isAtCapacity = false
let numTries = 0
do {
try {
await solveSimpleCaptchas(page)
const res = await page.$x("//div[contains(., 'ChatGPT is at capacity')]")
isAtCapacity = !!res?.length
if (isAtCapacity) {
if (++numTries >= retries) {
break
}
// try refreshing the page if chatgpt is at capacity
await page.reload({
waitUntil: 'networkidle2',
timeout: timeoutMs
})
await delay(pollingIntervalMs)
}
} catch (err) {
// ignore errors likely due to navigation
++numTries
break
}
} while (isAtCapacity)
if (isAtCapacity) {
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)
})
}
async function solveSimpleCaptchas (page) {
try {
const verifyYouAreHuman = await page.$('text=Verify you are human')
if (verifyYouAreHuman) {
logger.mark('encounter cloudflare simple captcha "Verify you are human"')
await delay(2000)
await verifyYouAreHuman.click({
delay: random.int(5, 25)
})
await delay(1000)
}
const verifyYouAreHumanCN = await page.$('text=确认您是真人')
if (verifyYouAreHumanCN) {
logger.mark('encounter cloudflare simple captcha "确认您是真人"')
await delay(2000)
await verifyYouAreHumanCN.click({
delay: random.int(5, 25)
})
await delay(1000)
}
const cloudflareButton = await page.$('.hcaptcha-box')
if (cloudflareButton) {
await delay(2000)
await cloudflareButton.click({
delay: random.int(5, 25)
})
await delay(1000)
}
} catch (err) {
// ignore errors
}
}