fix: remove useless thing

This commit is contained in:
ikechan8370 2024-03-09 23:37:47 +08:00
parent 5c544a5ca7
commit bd7aac0517
53 changed files with 350 additions and 2639 deletions

View file

@ -1,5 +1,5 @@
import plugin from '../../../lib/plugins/plugin.js' import plugin from '../../../lib/plugins/plugin.js'
import {Config} from "../utils/config.js"; import { Config } from '../utils/config.js'
const PLUGIN_CHAT = 'ChatGpt 对话' const PLUGIN_CHAT = 'ChatGpt 对话'
const PLUGIN_MANAGEMENT = 'ChatGPT-Plugin 管理' const PLUGIN_MANAGEMENT = 'ChatGPT-Plugin 管理'

View file

@ -5,7 +5,6 @@ import { Config, defaultOpenAIAPI } from '../utils/config.js'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { ChatGPTAPI } from '../utils/openai/chatgpt-api.js' import { ChatGPTAPI } from '../utils/openai/chatgpt-api.js'
import SydneyAIClient from '../utils/SydneyAIClient.js' import SydneyAIClient from '../utils/SydneyAIClient.js'
import { PoeClient } from '../utils/poe/index.js'
import AzureTTS from '../utils/tts/microsoft-azure.js' import AzureTTS from '../utils/tts/microsoft-azure.js'
import VoiceVoxTTS from '../utils/tts/voicevox.js' import VoiceVoxTTS from '../utils/tts/voicevox.js'
import { import {
@ -31,7 +30,6 @@ import {
renderUrl renderUrl
} from '../utils/common.js' } from '../utils/common.js'
import { ChatGPTPuppeteer } from '../utils/browser.js'
import { KeyvFile } from 'keyv-file' import { KeyvFile } from 'keyv-file'
import { OfficialChatGPTClient } from '../utils/message.js' import { OfficialChatGPTClient } from '../utils/message.js'
import fetch from 'node-fetch' import fetch from 'node-fetch'
@ -39,7 +37,7 @@ import { deleteConversation, getConversations, getLatestMessageIdByConversationI
import { convertSpeaker, speakers } from '../utils/tts.js' import { convertSpeaker, speakers } from '../utils/tts.js'
import ChatGLMClient from '../utils/chatglm.js' import ChatGLMClient from '../utils/chatglm.js'
import { convertFaces } from '../utils/face.js' import { convertFaces } from '../utils/face.js'
import { originalValues, ConversationManager } from '../model/conversation.js' import { ConversationManager, originalValues } from '../model/conversation.js'
import BingDrawClient from '../utils/BingDraw.js' import BingDrawClient from '../utils/BingDraw.js'
import XinghuoClient from '../utils/xinghuo/xinghuo.js' import XinghuoClient from '../utils/xinghuo/xinghuo.js'
import Bard from '../utils/bard.js' import Bard from '../utils/bard.js'
@ -795,10 +793,6 @@ export class chatgpt extends plugin {
key = `CHATGPT:CONVERSATIONS_CHATGLM:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` key = `CHATGPT:CONVERSATIONS_CHATGLM:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`
break break
} }
case 'browser': {
key = `CHATGPT:CONVERSATIONS_BROWSER:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`
break
}
case 'claude2': { case 'claude2': {
key = `CHATGPT:CLAUDE2_CONVERSATION:${e.sender.user_id}` key = `CHATGPT:CLAUDE2_CONVERSATION:${e.sender.user_id}`
break break
@ -877,7 +871,7 @@ export class chatgpt extends plugin {
// 字数超限直接返回 // 字数超限直接返回
return false return false
} }
if (use !== 'api3' && use !== 'poe') { if (use !== 'api3') {
previousConversation.conversation = { previousConversation.conversation = {
conversationId: chatMessage.conversationId conversationId: chatMessage.conversationId
} }
@ -1534,49 +1528,9 @@ export class chatgpt extends plugin {
user: e.sender.user_id, user: e.sender.user_id,
cache: cacheOptions cache: cacheOptions
}) })
let sendMessageResult = await this.chatGPTApi.sendMessage(prompt, conversation) return await this.chatGPTApi.sendMessage(prompt, conversation)
return sendMessageResult
} else if (use === 'poe') {
const cookie = await redis.get('CHATGPT:POE_TOKEN')
if (!cookie) {
throw new Error('未绑定Poe Cookie请使用#chatgpt设置Poe token命令绑定cookie')
}
let client = new PoeClient({
quora_cookie: cookie,
proxy: Config.proxy
})
await client.setCredentials()
await client.getChatId()
let ai = 'a2' // todo
await client.sendMsg(ai, prompt)
const response = await client.getResponse(ai)
return {
text: response.data
}
} else if (use === 'claude') { } else if (use === 'claude') {
// slack已经不可用移除 // slack已经不可用移除
// let client = new SlackClaudeClient({
// slackUserToken: Config.slackUserToken,
// slackChannelId: Config.slackChannelId
// })
// let conversationId = await redis.get(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`)
// if (!conversationId) {
// // 如果是新对话
// if (Config.slackClaudeEnableGlobalPreset && (useCast?.slack || Config.slackClaudeGlobalPreset)) {
// // 先发送设定
// let prompt = (useCast?.slack || Config.slackClaudeGlobalPreset)
// let emotion = await AzureTTS.getEmotionPrompt(e)
// if (emotion) {
// prompt = prompt + '\n' + emotion
// }
// await client.sendMessage(prompt, e)
// logger.info('claudeFirst:', prompt)
// }
// }
// let text = await client.sendMessage(prompt, e)
// return {
// text
// }
const client = new ClaudeAPIClient({ const client = new ClaudeAPIClient({
key: Config.claudeApiKey, key: Config.claudeApiKey,
model: Config.claudeApiModel || 'claude-3-sonnet-20240229', model: Config.claudeApiModel || 'claude-3-sonnet-20240229',
@ -2408,30 +2362,6 @@ export class chatgpt extends plugin {
this.reply('总额度:$' + hardLimit + '\n已经使用额度$' + totalUsage / 100 + '\n当前剩余额度$' + left + '\n到期日期(UTC)' + expiresAt) this.reply('总额度:$' + hardLimit + '\n已经使用额度$' + totalUsage / 100 + '\n当前剩余额度$' + left + '\n到期日期(UTC)' + expiresAt)
} }
/**
* #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)
}
/** /**
* 其他模式 * 其他模式
* @param e * @param e

View file

@ -4,7 +4,6 @@ import { generateHello } from '../utils/randomMessage.js'
import { generateVitsAudio } from '../utils/tts.js' import { generateVitsAudio } from '../utils/tts.js'
import fs from 'fs' import fs from 'fs'
import { emojiRegex, googleRequestUrl } from '../utils/emoj/index.js' import { emojiRegex, googleRequestUrl } from '../utils/emoj/index.js'
import fetch from 'node-fetch'
import { getImageOcrText, getImg, makeForwardMsg, mkdirs, renderUrl } from '../utils/common.js' import { getImageOcrText, getImg, makeForwardMsg, mkdirs, renderUrl } from '../utils/common.js'
import uploadRecord from '../utils/uploadRecord.js' import uploadRecord from '../utils/uploadRecord.js'
import { makeWordcloud } from '../utils/wordcloud/wordcloud.js' import { makeWordcloud } from '../utils/wordcloud/wordcloud.js'

View file

@ -99,13 +99,8 @@ export class ChatgptManagement extends plugin {
permission: 'master' permission: 'master'
}, },
{ {
reg: '^#chatgpt切换(Poe|poe)$', reg: '^#chatgpt切换(Claude|claude)$',
fnc: 'useClaudeBasedSolution', fnc: 'useClaudeAPIBasedSolution',
permission: 'master'
},
{
reg: '^#chatgpt切换(Claude|claude|slack)$',
fnc: 'useSlackClaudeBasedSolution',
permission: 'master' permission: 'master'
}, },
{ {
@ -923,23 +918,13 @@ azure语音Azure 语音是微软 Azure 平台提供的一项语音服务,
} }
} }
async useClaudeBasedSolution (e) { async useClaudeAPIBasedSolution () {
let use = await redis.get('CHATGPT:USE')
if (use !== 'poe') {
await redis.set('CHATGPT:USE', 'poe')
await this.reply('已切换到基于Quora\'s POE的解决方案')
} else {
await this.reply('当前已经是POE模式了')
}
}
async useSlackClaudeBasedSolution () {
let use = await redis.get('CHATGPT:USE') let use = await redis.get('CHATGPT:USE')
if (use !== 'claude') { if (use !== 'claude') {
await redis.set('CHATGPT:USE', 'claude') await redis.set('CHATGPT:USE', 'claude')
await this.reply('已切换到基于slack claude机器人的解决方案') await this.reply('已切换到基于ClaudeAPI的解决方案')
} else { } else {
await this.reply('当前已经是claude模式了') await this.reply('当前已经是Claude模式了')
} }
} }
@ -949,7 +934,7 @@ azure语音Azure 语音是微软 Azure 平台提供的一项语音服务,
await redis.set('CHATGPT:USE', 'claude2') await redis.set('CHATGPT:USE', 'claude2')
await this.reply('已切换到基于claude.ai的解决方案') await this.reply('已切换到基于claude.ai的解决方案')
} else { } else {
await this.reply('当前已经是claude2模式了') await this.reply('当前已经是claude.ai模式了')
} }
} }

View file

@ -1,10 +1,8 @@
import plugin from '../../../lib/plugins/plugin.js' import plugin from '../../../lib/plugins/plugin.js'
import fs from 'fs'
import _ from 'lodash'
import { Config } from '../utils/config.js' import { Config } from '../utils/config.js'
import { getMasterQQ, limitString, makeForwardMsg, maskQQ, getUin } from '../utils/common.js' import { getMasterQQ, limitString, makeForwardMsg, maskQQ, getUin } from '../utils/common.js'
import { deleteOnePrompt, getPromptByName, readPrompts, saveOnePrompt } from '../utils/prompts.js' import { deleteOnePrompt, getPromptByName, readPrompts, saveOnePrompt } from '../utils/prompts.js'
import AzureTTS from "../utils/tts/microsoft-azure.js"; import AzureTTS from '../utils/tts/microsoft-azure.js'
export class help extends plugin { export class help extends plugin {
constructor (e) { constructor (e) {
super({ super({

View file

@ -3,21 +3,11 @@ import plugin from '../../../lib/plugins/plugin.js'
import { createRequire } from 'module' import { createRequire } from 'module'
import _ from 'lodash' import _ from 'lodash'
import { Restart } from '../../other/restart.js' import { Restart } from '../../other/restart.js'
import fs from 'fs'
import {} from '../utils/common.js' import {} from '../utils/common.js'
const _path = process.cwd()
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const { exec, execSync } = require('child_process') const { exec, execSync } = require('child_process')
const checkAuth = async function (e) {
if (!e.isMaster) {
e.reply('只有主人才能命令ChatGPT哦~(*/ω\*)')
return false
}
return true
}
// 是否在更新中 // 是否在更新中
let uping = false let uping = false

View file

@ -1,7 +1,7 @@
import plugin from '../../../lib/plugins/plugin.js' import plugin from '../../../lib/plugins/plugin.js'
import { SunoClient } from '../client/SunoClient.js' import { SunoClient } from '../client/SunoClient.js'
import { Config } from '../utils/config.js' import { Config } from '../utils/config.js'
import { downloadFile, maskEmail } from '../utils/common.js' import { maskEmail } from '../utils/common.js'
import common from '../../../lib/common/common.js' import common from '../../../lib/common/common.js'
import lodash from 'lodash' import lodash from 'lodash'

View file

@ -89,13 +89,6 @@
"whitelist": [], "whitelist": [],
"blacklist": [], "blacklist": [],
"ttsRegex": "/匹配规则/匹配模式", "ttsRegex": "/匹配规则/匹配模式",
"slackUserToken": "",
"slackBotUserToken": "",
"slackSigningSecret": "",
"slackClaudeUserId": "",
"slackClaudeEnableGlobalPreset": true,
"slackClaudeGlobalPreset": "",
"slackClaudeSpecifiedChannel": "",
"cloudTranscode": "https://silk.201666.xyz", "cloudTranscode": "https://silk.201666.xyz",
"cloudRender": false, "cloudRender": false,
"cloudMode": "url", "cloudMode": "url",

View file

@ -9,7 +9,6 @@
"@fastify/static": "^6.9.0", "@fastify/static": "^6.9.0",
"@fastify/websocket": "^8.2.0", "@fastify/websocket": "^8.2.0",
"@google/generative-ai": "^0.1.1", "@google/generative-ai": "^0.1.1",
"@slack/bolt": "^3.13.2",
"asn1.js": "^5.0.0", "asn1.js": "^5.0.0",
"diff": "^5.1.0", "diff": "^5.1.0",
"emoji-strip": "^1.0.1", "emoji-strip": "^1.0.1",
@ -40,9 +39,6 @@
"node-silk": "^0.1.0", "node-silk": "^0.1.0",
"nodejs-pptx": "^1.2.4", "nodejs-pptx": "^1.2.4",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^3.11.174",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-recaptcha": "^3.6.8",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"sharp": "^0.32.3", "sharp": "^0.32.3",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },

View file

@ -1,23 +1,23 @@
import fetch, { import fetch, {
Headers, // Headers,
Request, // Request,
Response, // Response,
FormData FormData
} from 'node-fetch' } from 'node-fetch'
import crypto from 'crypto' import crypto from 'crypto'
import WebSocket from 'ws' import WebSocket from 'ws'
import { Config, pureSydneyInstruction } from './config.js' import { Config } from './config.js'
import { formatDate, getMasterQQ, isCN, getUserData, limitString } from './common.js' import { formatDate, getMasterQQ, isCN, getUserData, limitString } from './common.js'
import moment from 'moment' import moment from 'moment'
import { getProxy } from './proxy.js' import { getProxy } from './proxy.js'
import common from '../../../lib/common/common.js' import common from '../../../lib/common/common.js'
//
if (!globalThis.fetch) { // if (!globalThis.fetch) {
globalThis.fetch = fetch // globalThis.fetch = fetch
globalThis.Headers = Headers // globalThis.Headers = Headers
globalThis.Request = Request // globalThis.Request = Request
globalThis.Response = Response // globalThis.Response = Response
} // }
// workaround for ver 7.x and ver 5.x // workaround for ver 7.x and ver 5.x
let proxy = getProxy() let proxy = getProxy()

View file

@ -1,373 +1,373 @@
// https://github.com/EvanZhouDev/bard-ai // https://github.com/EvanZhouDev/bard-ai
class Bard { class Bard {
static JSON = "json"; static JSON = 'json'
static MD = "markdown"; static MD = 'markdown'
// ID derived from Cookie // ID derived from Cookie
SNlM0e; SNlM0e
// HTTPS Headers // HTTPS Headers
#headers; #headers
// Resolution status of initialization call // Resolution status of initialization call
#initPromise; #initPromise
#bardURL = "https://bard.google.com"; #bardURL = 'https://bard.google.com'
// Wether or not to log events to console // Wether or not to log events to console
#verbose = false; #verbose = false
// Fetch function // Fetch function
#fetch = fetch; #fetch = fetch
constructor(cookie, config) { constructor (cookie, config) {
// Register some settings // Register some settings
if (config?.verbose == true) this.#verbose = true; if (config?.verbose == true) this.#verbose = true
if (config?.fetch) this.#fetch = config.fetch; if (config?.fetch) this.#fetch = config.fetch
// 可变更访问地址,利用反向代理绕过区域限制 // 可变更访问地址,利用反向代理绕过区域限制
if (config?.bardURL) this.#bardURL = config.bardURL; if (config?.bardURL) this.#bardURL = config.bardURL
// If a Cookie is provided, initialize // If a Cookie is provided, initialize
if (cookie) { if (cookie) {
this.#initPromise = this.#init(cookie); this.#initPromise = this.#init(cookie)
} else { } else {
throw new Error("Please provide a Cookie when initializing Bard."); throw new Error('Please provide a Cookie when initializing Bard.')
} }
this.cookie = cookie; this.cookie = cookie
} }
// You can also choose to initialize manually // You can also choose to initialize manually
async #init(cookie) { async #init (cookie) {
this.#verbose && console.log("🚀 Starting intialization"); this.#verbose && console.log('🚀 Starting intialization')
// Assign headers // Assign headers
this.#headers = { this.#headers = {
Host: this.#bardURL.match(/^https?:\/\/([^\/]+)\/?$/)[1], Host: this.#bardURL.match(/^https?:\/\/([^\/]+)\/?$/)[1],
"X-Same-Domain": "1", 'X-Same-Domain': '1',
"User-Agent": 'User-Agent':
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36", 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
Origin: this.#bardURL, Origin: this.#bardURL,
Referer: this.#bardURL, Referer: this.#bardURL,
Cookie: (typeof cookie === "object") ? (Object.entries(cookie).map(([key, val]) => `${key}=${val};`).join("")) : ("__Secure-1PSID=" + cookie), Cookie: (typeof cookie === 'object') ? (Object.entries(cookie).map(([key, val]) => `${key}=${val};`).join('')) : ('__Secure-1PSID=' + cookie)
}; }
let responseText; let responseText
// Attempt to retrieve SNlM0e // Attempt to retrieve SNlM0e
try { try {
this.#verbose && this.#verbose &&
console.log("🔒 Authenticating your Google account"); console.log('🔒 Authenticating your Google account')
responseText = await this.#fetch(this.#bardURL, { responseText = await this.#fetch(this.#bardURL, {
method: "GET", method: 'GET',
headers: this.#headers, headers: this.#headers,
credentials: "include", credentials: 'include'
}) })
.then((response) => response.text()) .then((response) => response.text())
} catch (e) { } catch (e) {
// Failure to get server // Failure to get server
throw new Error( throw new Error(
"Could not fetch Google Bard. You may be disconnected from internet: " + 'Could not fetch Google Bard. You may be disconnected from internet: ' +
e e
); )
} }
try { try {
const SNlM0e = responseText.match(/SNlM0e":"(.*?)"/)[1]; const SNlM0e = responseText.match(/SNlM0e":"(.*?)"/)[1]
// Assign SNlM0e and return it // Assign SNlM0e and return it
this.SNlM0e = SNlM0e; this.SNlM0e = SNlM0e
this.#verbose && console.log("✅ Initialization finished\n"); this.#verbose && console.log('✅ Initialization finished\n')
return SNlM0e; return SNlM0e
} catch { } catch {
throw new Error( throw new Error(
"Could not use your Cookie. Make sure that you copied correctly the Cookie with name __Secure-1PSID exactly. If you are sure your cookie is correct, you may also have reached your rate limit." 'Could not use your Cookie. Make sure that you copied correctly the Cookie with name __Secure-1PSID exactly. If you are sure your cookie is correct, you may also have reached your rate limit.'
); )
} }
} }
async #uploadImage(name, buffer) { async #uploadImage (name, buffer) {
this.#verbose && console.log("🖼️ Starting image processing"); this.#verbose && console.log('🖼️ Starting image processing')
let size = buffer.byteLength; let size = buffer.byteLength
let formBody = [ let formBody = [
`${encodeURIComponent("File name")}=${encodeURIComponent([name])}`, `${encodeURIComponent('File name')}=${encodeURIComponent([name])}`
]; ]
try { try {
this.#verbose && this.#verbose &&
console.log("💻 Finding Google server destination"); console.log('💻 Finding Google server destination')
let response = await this.#fetch( let response = await this.#fetch(
"https://content-push.googleapis.com/upload/", 'https://content-push.googleapis.com/upload/',
{ {
method: "POST", method: 'POST',
headers: { headers: {
"X-Goog-Upload-Command": "start", 'X-Goog-Upload-Command': 'start',
"X-Goog-Upload-Protocol": "resumable", 'X-Goog-Upload-Protocol': 'resumable',
"X-Goog-Upload-Header-Content-Length": size, 'X-Goog-Upload-Header-Content-Length': size,
"X-Tenant-Id": "bard-storage", 'X-Tenant-Id': 'bard-storage',
"Push-Id": "feeds/mcudyrk2a4khkz", 'Push-Id': 'feeds/mcudyrk2a4khkz'
}, },
body: formBody, body: formBody,
credentials: "include", credentials: 'include'
} }
); )
const uploadUrl = response.headers.get("X-Goog-Upload-URL"); const uploadUrl = response.headers.get('X-Goog-Upload-URL')
this.#verbose && console.log("📤 Sending your image"); this.#verbose && console.log('📤 Sending your image')
response = await this.#fetch(uploadUrl, { response = await this.#fetch(uploadUrl, {
method: "POST", method: 'POST',
headers: { headers: {
"X-Goog-Upload-Command": "upload, finalize", 'X-Goog-Upload-Command': 'upload, finalize',
"X-Goog-Upload-Offset": 0, 'X-Goog-Upload-Offset': 0,
"X-Tenant-Id": "bard-storage", 'X-Tenant-Id': 'bard-storage'
}, },
body: buffer, body: buffer,
credentials: "include", credentials: 'include'
}); })
const imageFileLocation = await response.text(); const imageFileLocation = await response.text()
this.#verbose && console.log("✅ Image finished working\n"); this.#verbose && console.log('✅ Image finished working\n')
return imageFileLocation; return imageFileLocation
} catch (e) { } catch (e) {
throw new Error( throw new Error(
"Could not fetch Google Bard. You may be disconnected from internet: " + 'Could not fetch Google Bard. You may be disconnected from internet: ' +
e e
); )
} }
} }
// Query Bard // Query Bard
async #query(message, config) { async #query (message, config) {
let formatMarkdown = (text, images) => { let formatMarkdown = (text, images) => {
if (!images) return text; if (!images) return text
for (let imageData of images) { for (let imageData of images) {
const formattedTag = `!${imageData.tag}(${imageData.url})`; const formattedTag = `!${imageData.tag}(${imageData.url})`
text = text.replace( text = text.replace(
new RegExp(`(?!\\!)\\[${imageData.tag.slice(1, -1)}\\]`), new RegExp(`(?!\\!)\\[${imageData.tag.slice(1, -1)}\\]`),
formattedTag formattedTag
); )
}
return text;
} }
let { ids, imageBuffer } = config; return text
}
// Wait until after init let { ids, imageBuffer } = config
await this.#initPromise;
this.#verbose && console.log("🔎 Starting Bard Query"); // Wait until after init
await this.#initPromise
// If user has not run init this.#verbose && console.log('🔎 Starting Bard Query')
if (!this.SNlM0e) {
throw new Error(
"Please initialize Bard first. If you haven't passed in your Cookie into the class, run Bard.init(cookie)."
);
}
this.#verbose && console.log("🏗️ Building Request"); // If user has not run init
// HTTPS parameters if (!this.SNlM0e) {
const params = { throw new Error(
bl: "boq_assistant-bard-web-server_20230711.08_p0", "Please initialize Bard first. If you haven't passed in your Cookie into the class, run Bard.init(cookie)."
_reqID: ids?._reqID ?? "0", )
rt: "c", }
};
// If IDs are provided, but doesn't have every one of the expected IDs, error this.#verbose && console.log('🏗️ Building Request')
const messageStruct = [ // HTTPS parameters
[message], const params = {
null, bl: 'boq_assistant-bard-web-server_20230711.08_p0',
[null, null, null], _reqID: ids?._reqID ?? '0',
]; rt: 'c'
}
if (imageBuffer) { // If IDs are provided, but doesn't have every one of the expected IDs, error
let imageLocation = await this.#uploadImage( const messageStruct = [
`bard-ai_upload`, [message],
imageBuffer null,
); [null, null, null]
messageStruct[0].push(0, null, [ ]
[[imageLocation, 1], "bard-ai_upload"],
]);
}
if (ids) { if (imageBuffer) {
const { conversationID, responseID, choiceID } = ids; let imageLocation = await this.#uploadImage(
messageStruct[2] = [conversationID, responseID, choiceID]; 'bard-ai_upload',
} imageBuffer
)
messageStruct[0].push(0, null, [
[[imageLocation, 1], 'bard-ai_upload']
])
}
// HTTPs data if (ids) {
const data = { const { conversationID, responseID, choiceID } = ids
"f.req": JSON.stringify([null, JSON.stringify(messageStruct)]), messageStruct[2] = [conversationID, responseID, choiceID]
at: this.SNlM0e, }
};
// URL that we are submitting to // HTTPs data
const url = new URL( const data = {
"/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate", 'f.req': JSON.stringify([null, JSON.stringify(messageStruct)]),
this.#bardURL at: this.SNlM0e
); }
// Append parameters to the URL // URL that we are submitting to
for (const key in params) { const url = new URL(
url.searchParams.append(key, params[key]); '/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate',
} this.#bardURL
)
// Encode the data // Append parameters to the URL
const formBody = Object.entries(data) for (const key in params) {
.map( url.searchParams.append(key, params[key])
([property, value]) => }
// Encode the data
const formBody = Object.entries(data)
.map(
([property, value]) =>
`${encodeURIComponent(property)}=${encodeURIComponent( `${encodeURIComponent(property)}=${encodeURIComponent(
value value
)}` )}`
) )
.join("&"); .join('&')
this.#verbose && console.log("💭 Sending message to Bard"); this.#verbose && console.log('💭 Sending message to Bard')
// Send the fetch request // Send the fetch request
const chatData = await this.#fetch(url.toString(), { const chatData = await this.#fetch(url.toString(), {
method: "POST", method: 'POST',
headers: this.#headers, headers: this.#headers,
body: formBody, body: formBody,
credentials: "include", credentials: 'include'
})
.then((response) => {
return response.text()
}) })
.then((response) => { .then((text) => {
return response.text(); return JSON.parse(text.split('\n')[3])[0][2]
}) })
.then((text) => { .then((rawData) => JSON.parse(rawData))
return JSON.parse(text.split("\n")[3])[0][2];
})
.then((rawData) => JSON.parse(rawData));
this.#verbose && console.log("🧩 Parsing output"); this.#verbose && console.log('🧩 Parsing output')
// Get first Bard-recommended answer // Get first Bard-recommended answer
const answer = chatData[4][0]; const answer = chatData[4][0]
// Text of that answer // Text of that answer
const text = answer[1][0]; const text = answer[1][0]
// Get data about images in that answer // Get data about images in that answer
const images = const images =
answer[4]?.map((x) => ({ answer[4]?.map((x) => ({
tag: x[2], tag: x[2],
url: x[3][0][0], url: x[3][0][0],
info: { info: {
raw: x[0][0][0], raw: x[0][0][0],
source: x[1][0][0], source: x[1][0][0],
alt: x[0][4], alt: x[0][4],
website: x[1][1], website: x[1][1],
favicon: x[1][3], favicon: x[1][3]
}, }
})) ?? []; })) ?? []
this.#verbose && console.log("✅ All done!\n"); this.#verbose && console.log('✅ All done!\n')
// Put everything together and return // Put everything together and return
return { return {
content: formatMarkdown(text, images), content: formatMarkdown(text, images),
images: images, images,
ids: { ids: {
conversationID: chatData[1][0], conversationID: chatData[1][0],
responseID: chatData[1][1], responseID: chatData[1][1],
choiceID: answer[0], choiceID: answer[0],
_reqID: String(parseInt(ids?._reqID ?? 0) + 100000), _reqID: String(parseInt(ids?._reqID ?? 0) + 100000)
}, }
}; }
} }
async #parseConfig(config) { async #parseConfig (config) {
let result = { let result = {
useJSON: false, useJSON: false,
imageBuffer: undefined, // Returns as {extension, filename} imageBuffer: undefined, // Returns as {extension, filename}
ids: undefined, ids: undefined
}; }
// Verify that format is one of the two types // Verify that format is one of the two types
if (config?.format) { if (config?.format) {
switch (config.format) { switch (config.format) {
case Bard.JSON: case Bard.JSON:
result.useJSON = true; result.useJSON = true
break; break
case Bard.MD: case Bard.MD:
result.useJSON = false; result.useJSON = false
break; break
default: default:
throw new Error( throw new Error(
"Format can obly be Bard.JSON for JSON output or Bard.MD for Markdown output." 'Format can obly be Bard.JSON for JSON output or Bard.MD for Markdown output.'
); )
}
} }
}
// Verify that the image passed in is either a path to a jpeg, jpg, png, or webp, or that it is a Buffer // Verify that the image passed in is either a path to a jpeg, jpg, png, or webp, or that it is a Buffer
if (config?.image) { if (config?.image) {
if ( if (
config.image instanceof ArrayBuffer config.image instanceof ArrayBuffer
) { ) {
result.imageBuffer = config.image; result.imageBuffer = config.image
} else if ( } else if (
typeof config.image === "string" && typeof config.image === 'string' &&
/\.(jpeg|jpg|png|webp)$/.test(config.image) /\.(jpeg|jpg|png|webp)$/.test(config.image)
) { ) {
let fs; let fs
try { try {
fs = await import("fs") fs = await import('fs')
} catch { } catch {
throw new Error( throw new Error(
"Loading from an image file path is not supported in a browser environment.", 'Loading from an image file path is not supported in a browser environment.'
); )
} }
result.imageBuffer = fs.readFileSync( result.imageBuffer = fs.readFileSync(
config.image, config.image
).buffer; ).buffer
} else { } else {
throw new Error( throw new Error(
"Provide your image as a file path to a .jpeg, .jpg, .png, or .webp, or a Buffer." 'Provide your image as a file path to a .jpeg, .jpg, .png, or .webp, or a Buffer.'
); )
}
} }
}
// Verify that all values in IDs exist // Verify that all values in IDs exist
if (config?.ids) { if (config?.ids) {
if (config.ids.conversationID && config.ids.responseID && config.ids.choiceID && config.ids._reqID) { if (config.ids.conversationID && config.ids.responseID && config.ids.choiceID && config.ids._reqID) {
result.ids = config.ids; result.ids = config.ids
} else { } else {
throw new Error( throw new Error(
"Please provide the IDs exported exactly as given." 'Please provide the IDs exported exactly as given.'
); )
}
} }
return result; }
return result
} }
// Ask Bard a question! // Ask Bard a question!
async ask(message, config) { async ask (message, config) {
let { useJSON, imageBuffer, ids } = await this.#parseConfig(config); let { useJSON, imageBuffer, ids } = await this.#parseConfig(config)
let response = await this.#query(message, { imageBuffer, ids }); let response = await this.#query(message, { imageBuffer, ids })
return useJSON ? response : response.content; return useJSON ? response : response.content
} }
createChat(ids) { createChat (ids) {
let bard = this; let bard = this
class Chat { class Chat {
ids = ids; ids = ids
async ask(message, config) { async ask (message, config) {
let { useJSON, imageBuffer } = await bard.#parseConfig(config); let { useJSON, imageBuffer } = await bard.#parseConfig(config)
let response = await bard.#query(message, { let response = await bard.#query(message, {
imageBuffer, imageBuffer,
ids: this.ids, ids: this.ids
}); })
this.ids = response.ids; this.ids = response.ids
return useJSON ? response : response.content; return useJSON ? response : response.content
}
export() {
return this.ids;
}
} }
return new Chat(); export () {
return this.ids
}
}
return new Chat()
} }
} }
export default Bard; export default Bard

View file

@ -1,7 +1,7 @@
import fetch from 'node-fetch' import fetch from 'node-fetch'
// this file is deprecated // this file is deprecated
import {Config} from './config.js' import { Config } from './config.js'
import HttpsProxyAgent from 'https-proxy-agent' import HttpsProxyAgent from 'https-proxy-agent'
const newFetch = (url, options = {}) => { const newFetch = (url, options = {}) => {

View file

@ -1,10 +1,5 @@
import lodash from 'lodash' import lodash from 'lodash'
import { Config } from '../utils/config.js' import { Config } from './config.js'
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
import { getOpenAIAuth } from './openai-auth.js'
import { v4 as uuidv4 } from 'uuid'
import common from '../../../lib/common/common.js'
const chatUrl = 'https://chat.openai.com/chat'
let puppeteer = {} let puppeteer = {}
class Puppeteer { class Puppeteer {
@ -48,19 +43,9 @@ class Puppeteer {
async initPupp () { async initPupp () {
if (!lodash.isEmpty(puppeteer)) return puppeteer if (!lodash.isEmpty(puppeteer)) return puppeteer
puppeteer = (await import('puppeteer-extra')).default puppeteer = (await import('puppeteer')).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
} }
@ -109,25 +94,10 @@ export class ChatGPTPuppeteer extends Puppeteer {
constructor (opts = {}) { constructor (opts = {}) {
super() super()
const { const {
email, debug = false
password,
markdown = true,
debug = false,
isGoogleLogin = false,
minimize = true,
captchaToken,
executablePath
} = opts } = opts
this._email = email
this._password = password
this._markdown = !!markdown
this._debug = !!debug this._debug = !!debug
this._isGoogleLogin = !!isGoogleLogin
this._minimize = !!minimize
this._captchaToken = captchaToken
this._executablePath = executablePath
} }
async getBrowser () { async getBrowser () {
@ -138,394 +108,6 @@ export class ChatGPTPuppeteer extends Puppeteer {
} }
} }
async init () {
// if (this.inited) {
// return true
// }
logger.info('init chatgpt browser')
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))
}
// const url = this._page.url().replace(/\/$/, '')
// bypass annoying popup modals
await this._page.evaluateOnNewDocument(() => {
window.localStorage.setItem('oai/apps/hasSeenOnboarding/chat', 'true')
const chatGPTUpdateDates = ['2022-12-15', '2022-12-19', '2023-01-09', '2023-01-30', '2023-02-10']
chatGPTUpdateDates.forEach(date => {
window.localStorage.setItem(
`oai/apps/hasSeenReleaseAnnouncement/${date}`,
'true'
)
})
})
await this._page.goto(chatUrl, {
waitUntil: 'networkidle2'
})
let timeout = 30000
try {
while (timeout > 0 && (await this._page.title()).toLowerCase().indexOf('moment') > -1) {
// if meet captcha
if (Config['2captchaToken']) {
await this._page.solveRecaptchas()
}
await common.sleep(300)
timeout = timeout - 300
}
} catch (e) {
// navigation后获取title会报错报错说明已经在navigation了正合我意。
}
if (timeout < 0) {
logger.error('wait for cloudflare navigation timeout. 可能遇见验证码')
throw new Error('wait for cloudflare navigation timeout. 可能遇见验证码')
}
try {
await this._page.waitForNavigation({ timeout: 3000 })
} catch (e) {}
if (!await this.getIsAuthenticated()) {
await redis.del('CHATGPT:RAW_COOKIES')
logger.info('需要登录,准备进行自动化登录')
await getOpenAIAuth({
email: this._email,
password: this._password,
browser: this.browser,
page: this._page,
isGoogleLogin: this._isGoogleLogin
})
logger.info('登录完成')
} else {
logger.info('无需登录')
}
} 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 common.sleep(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: Config.chromeTimeoutMS // 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 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()
)
throw err
}
let timeout = 100000
if (isAuthenticated) {
while (!this._accessToken) {
// wait for async response hook result
await common.sleep(300)
timeout = timeout - 300
if (timeout < 0) {
const error = new Error('Not signed in')
error.statusCode = 401
throw error
}
}
} else if (!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: Config.plus ? Config.useGPT4 ? 'gpt-4' : 'text-davinci-002-render-sha' : 'text-davinci-002-render-sha',
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 {
text: result.response,
conversationId: result.conversationResponse.conversation_id,
id: messageId,
parentMessageId
}
// 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 common.sleep(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 () { async close () {
if (this.browser) { if (this.browser) {
await this.browser.close() await this.browser.close()
@ -533,510 +115,6 @@ export class ChatGPTPuppeteer extends Puppeteer {
this._page = null this._page = null
this.browser = 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 _checkJson = JSON.parse(data)
} catch (error) {
console.log('warning: parse error.')
return
}
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()
}
}
console.log({ pTimeout })
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 pTimeout (promise, option) {
// return await pTimeout(promise, option)
// }
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
)
}
// @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 Error(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
}
/**
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)
}
/**
TODO: Remove AbortError and just throw DOMException when targeting Node 18.
*/
function getDOMException (errorMessage) {
return globalThis.DOMException === undefined
? new Error(errorMessage)
: new DOMException(errorMessage)
}
}

View file

@ -1,4 +1,3 @@
export async function getChatHistoryGroup (e, num) { export async function getChatHistoryGroup (e, num) {
// if (e.adapter === 'shamrock') { // if (e.adapter === 'shamrock') {
// return await e.group.getChatHistory(0, num, false) // return await e.group.getChatHistory(0, num, false)

View file

@ -187,7 +187,7 @@ const defaultConfig = {
claudeSystemPrompt: '', // claude api 设定 claudeSystemPrompt: '', // claude api 设定
translateSource: 'openai', translateSource: 'openai',
enableMd: false, // 第三方md非QQBot。需要适配器实现segment.markdown和segment.button方可使用否则不建议开启会造成各种错误 enableMd: false, // 第三方md非QQBot。需要适配器实现segment.markdown和segment.button方可使用否则不建议开启会造成各种错误
enableToolbox: false, // 默认关闭工具箱节省占用和加速启动 enableToolbox: true, // 默认关闭工具箱节省占用和加速启动
version: 'v2.8.1' version: 'v2.8.1'
} }
const _path = process.cwd() const _path = process.cwd()

View file

@ -1,7 +1,7 @@
export function decrypt (jwtToken) { export function decrypt (jwtToken) {
const [encodedHeader, encodedPayload, signature] = jwtToken.split('.') const [encodedHeader, encodedPayload, signature] = jwtToken.split('.')
const decodedHeader = Buffer.from(encodedHeader, 'base64').toString('utf-8') // const decodedHeader = Buffer.from(encodedHeader, 'base64').toString('utf-8')
const decodedPayload = Buffer.from(encodedPayload, 'base64').toString('utf-8') const decodedPayload = Buffer.from(encodedPayload, 'base64').toString('utf-8')
return decodedPayload return decodedPayload

View file

@ -1,281 +0,0 @@
import { Config } from '../utils/config.js'
import random from 'random'
import common from '../../../lib/common/common.js'
let hasRecaptchaPlugin = !!Config['2captchaToken']
export async function getOpenAIAuth (opt) {
let {
email,
password,
browser,
page,
timeoutMs = Config.chromeTimeoutMS,
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: timeoutMs / 3 })
)
} catch (e) {
await checkForChatGPTAtCapacity(page)
}
retry--
}
await waitForConditionOrAtCapacity(page, () =>
page.waitForSelector('#__next .btn-primary', { timeout: timeoutMs / 3 })
)
await common.sleep(500)
// click login button and wait for navigation to finish
do {
await Promise.all([
page.waitForNavigation({
waitUntil: 'networkidle2',
timeout: timeoutMs
}),
page.click('#__next .btn-primary')
])
await common.sleep(1000)
} while (page.url().endsWith('/auth/login'))
logger.mark('进入登录页面')
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 common.sleep(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 common.sleep(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 = Config.chromeTimeoutMS, // 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 common.sleep(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 common.sleep(2000)
await verifyYouAreHuman.click({
delay: random.int(5, 25)
})
await common.sleep(1000)
}
const verifyYouAreHumanCN = await page.$('text=确认您是真人')
if (verifyYouAreHumanCN) {
logger.mark('encounter cloudflare simple captcha "确认您是真人"')
await common.sleep(2000)
await verifyYouAreHumanCN.click({
delay: random.int(5, 25)
})
await common.sleep(1000)
}
const cloudflareButton = await page.$('.hcaptcha-box')
if (cloudflareButton) {
await common.sleep(2000)
await cloudflareButton.click({
delay: random.int(5, 25)
})
await common.sleep(1000)
}
} catch (err) {
// ignore errors
}
}

View file

@ -1,48 +0,0 @@
import fetch from 'node-fetch'
import { readFileSync, writeFile } from 'fs'
const scrape = async (pbCookie, proxy) => {
let option = { headers: { cookie: `${pbCookie}` } }
if (proxy) {
option.agent = proxy
}
const _setting = await fetch(
'https://poe.com/api/settings',
option
)
if (_setting.status !== 200) throw new Error('Failed to fetch token')
const appSettings = await _setting.json()
console.log(appSettings)
const { tchannelData: { channel: channelName } } = appSettings
return {
channelName,
appSettings,
formKey: appSettings.formKey
}
}
const getUpdatedSettings = async (channelName, pbCookie, proxy) => {
let option = { headers: { cookie: `${pbCookie}` } }
if (proxy) {
option.agent = proxy
}
const _setting = await fetch(
`https://poe.com/api/settings?channel=${channelName}`,
option
)
if (_setting.status !== 200) throw new Error('Failed to fetch token')
const appSettings = await _setting.json()
const { tchannelData: { minSeq } } = appSettings
const credentials = JSON.parse(readFileSync('config.json', 'utf8'))
credentials.app_settings.tchannelData.minSeq = minSeq
writeFile('config.json', JSON.stringify(credentials, null, 4), function (err) {
if (err) {
console.log(err)
}
})
return {
minSeq
}
}
export { scrape, getUpdatedSettings }

View file

@ -1,52 +0,0 @@
mutation AddHumanMessageMutation(
$chatId: BigInt!
$bot: String!
$query: String!
$source: MessageSource
$withChatBreak: Boolean! = false
) {
messageCreateWithStatus(
chatId: $chatId
bot: $bot
query: $query
source: $source
withChatBreak: $withChatBreak
) {
message {
id
__typename
messageId
text
linkifiedText
authorNickname
state
vote
voteReason
creationTime
suggestedReplies
chat {
id
shouldShowDisclaimer
}
}
messageLimit{
canSend
numMessagesRemaining
resetTime
shouldShowReminder
}
chatBreak {
id
__typename
messageId
text
linkifiedText
authorNickname
state
vote
voteReason
creationTime
suggestedReplies
}
}
}

View file

@ -1,17 +0,0 @@
mutation AddMessageBreakMutation($chatId: BigInt!) {
messageBreakCreate(chatId: $chatId) {
message {
id
__typename
messageId
text
linkifiedText
authorNickname
state
vote
voteReason
creationTime
suggestedReplies
}
}
}

View file

@ -1,7 +0,0 @@
mutation AutoSubscriptionMutation($subscriptions: [AutoSubscriptionQuery!]!) {
autoSubscribe(subscriptions: $subscriptions) {
viewer {
id
}
}
}

View file

@ -1,8 +0,0 @@
fragment BioFragment on Viewer {
id
poeUser {
id
uid
bio
}
}

View file

@ -1,5 +0,0 @@
subscription ChatAddedSubscription {
chatAdded {
...ChatFragment
}
}

View file

@ -1,6 +0,0 @@
fragment ChatFragment on Chat {
id
chatId
defaultBotNickname
shouldShowDisclaimer
}

View file

@ -1,26 +0,0 @@
query ChatPaginationQuery($bot: String!, $before: String, $last: Int! = 10) {
chatOfBot(bot: $bot) {
id
__typename
messagesConnection(before: $before, last: $last) {
pageInfo {
hasPreviousPage
}
edges {
node {
id
__typename
messageId
text
linkifiedText
authorNickname
state
vote
voteReason
creationTime
suggestedReplies
}
}
}
}
}

View file

@ -1,8 +0,0 @@
query ChatViewQuery($bot: String!) {
chatOfBot(bot: $bot) {
id
chatId
defaultBotNickname
shouldShowDisclaimer
}
}

View file

@ -1,7 +0,0 @@
mutation DeleteHumanMessagesMutation($messageIds: [BigInt!]!) {
messagesDelete(messageIds: $messageIds) {
viewer {
id
}
}
}

View file

@ -1,8 +0,0 @@
fragment HandleFragment on Viewer {
id
poeUser {
id
uid
handle
}
}

View file

@ -1,13 +0,0 @@
mutation LoginWithVerificationCodeMutation(
$verificationCode: String!
$emailAddress: String
$phoneNumber: String
) {
loginWithVerificationCode(
verificationCode: $verificationCode
emailAddress: $emailAddress
phoneNumber: $phoneNumber
) {
status
}
}

View file

@ -1,5 +0,0 @@
subscription MessageAddedSubscription($chatId: BigInt!) {
messageAdded(chatId: $chatId) {
...MessageFragment
}
}

View file

@ -1,6 +0,0 @@
subscription MessageDeletedSubscription($chatId: BigInt!) {
messageDeleted(chatId: $chatId) {
id
messageId
}
}

View file

@ -1,13 +0,0 @@
fragment MessageFragment on Message {
id
__typename
messageId
text
linkifiedText
authorNickname
state
vote
voteReason
creationTime
suggestedReplies
}

View file

@ -1,7 +0,0 @@
mutation MessageRemoveVoteMutation($messageId: BigInt!) {
messageRemoveVote(messageId: $messageId) {
message {
...MessageFragment
}
}
}

View file

@ -1,7 +0,0 @@
mutation MessageSetVoteMutation($messageId: BigInt!, $voteType: VoteType!, $reason: String) {
messageSetVote(messageId: $messageId, voteType: $voteType, reason: $reason) {
message {
...MessageFragment
}
}
}

View file

@ -1,12 +0,0 @@
mutation SendVerificationCodeForLoginMutation(
$emailAddress: String
$phoneNumber: String
) {
sendVerificationCode(
verificationReason: login
emailAddress: $emailAddress
phoneNumber: $phoneNumber
) {
status
}
}

View file

@ -1,9 +0,0 @@
mutation ShareMessagesMutation(
$chatId: BigInt!
$messageIds: [BigInt!]!
$comment: String
) {
messagesShare(chatId: $chatId, messageIds: $messageIds, comment: $comment) {
shareCode
}
}

View file

@ -1,13 +0,0 @@
mutation SignupWithVerificationCodeMutation(
$verificationCode: String!
$emailAddress: String
$phoneNumber: String
) {
signupWithVerificationCode(
verificationCode: $verificationCode
emailAddress: $emailAddress
phoneNumber: $phoneNumber
) {
status
}
}

View file

@ -1,7 +0,0 @@
mutation StaleChatUpdateMutation($chatId: BigInt!) {
staleChatUpdate(chatId: $chatId) {
message {
...MessageFragment
}
}
}

View file

@ -1,3 +0,0 @@
query SummarizePlainPostQuery($comment: String!) {
summarizePlainPost(comment: $comment)
}

View file

@ -1,3 +0,0 @@
query SummarizeQuotePostQuery($comment: String, $quotedPostId: BigInt!) {
summarizeQuotePost(comment: $comment, quotedPostId: $quotedPostId)
}

View file

@ -1,3 +0,0 @@
query SummarizeSharePostQuery($comment: String!, $chatId: BigInt!, $messageIds: [BigInt!]!) {
summarizeSharePost(comment: $comment, chatId: $chatId, messageIds: $messageIds)
}

View file

@ -1,14 +0,0 @@
fragment UserSnippetFragment on PoeUser {
id
uid
bio
handle
fullName
viewerIsFollowing
isPoeOnlyUser
profilePhotoURLTiny: profilePhotoUrl(size: tiny)
profilePhotoURLSmall: profilePhotoUrl(size: small)
profilePhotoURLMedium: profilePhotoUrl(size: medium)
profilePhotoURLLarge: profilePhotoUrl(size: large)
isFollowable
}

View file

@ -1,21 +0,0 @@
query ViewerInfoQuery {
viewer {
id
uid
...ViewerStateFragment
...BioFragment
...HandleFragment
hasCompletedMultiplayerNux
poeUser {
id
...UserSnippetFragment
}
messageLimit{
canSend
numMessagesRemaining
resetTime
shouldShowReminder
}
}
}

View file

@ -1,30 +0,0 @@
fragment ViewerStateFragment on Viewer {
id
__typename
iosMinSupportedVersion: integerGate(gateName: "poe_ios_min_supported_version")
iosMinEncouragedVersion: integerGate(
gateName: "poe_ios_min_encouraged_version"
)
macosMinSupportedVersion: integerGate(
gateName: "poe_macos_min_supported_version"
)
macosMinEncouragedVersion: integerGate(
gateName: "poe_macos_min_encouraged_version"
)
showPoeDebugPanel: booleanGate(gateName: "poe_show_debug_panel")
enableCommunityFeed: booleanGate(gateName: "enable_poe_shares_feed")
linkifyText: booleanGate(gateName: "poe_linkify_response")
enableSuggestedReplies: booleanGate(gateName: "poe_suggested_replies")
removeInviteLimit: booleanGate(gateName: "poe_remove_invite_limit")
enableInAppPurchases: booleanGate(gateName: "poe_enable_in_app_purchases")
availableBots {
nickname
displayName
profilePicture
isDown
disclaimer
subtitle
poweredBy
}
}

View file

@ -1,5 +0,0 @@
subscription ViewerStateUpdatedSubscription {
viewerStateUpdated {
...ViewerStateFragment
}
}

View file

@ -1,299 +0,0 @@
import { readFileSync } from 'fs'
import { scrape } from './credential.js'
import fetch from 'node-fetch'
import crypto from 'crypto'
import { Config } from '../config.js'
let proxy
if (Config.proxy) {
try {
proxy = (await import('https-proxy-agent')).default
} catch (e) {
console.warn('未安装https-proxy-agent请在插件目录下执行pnpm add https-proxy-agent')
}
}
// used when test as a single file
// const _path = process.cwd()
const _path = process.cwd() + '/plugins/chatgpt-plugin/utils/poe'
const gqlDir = `${_path}/graphql`
const queries = {
// chatViewQuery: readFileSync(gqlDir + '/ChatViewQuery.graphql', 'utf8'),
addMessageBreakMutation: readFileSync(gqlDir + '/AddMessageBreakMutation.graphql', 'utf8'),
chatPaginationQuery: readFileSync(gqlDir + '/ChatPaginationQuery.graphql', 'utf8'),
addHumanMessageMutation: readFileSync(gqlDir + '/AddHumanMessageMutation.graphql', 'utf8'),
loginMutation: readFileSync(gqlDir + '/LoginWithVerificationCodeMutation.graphql', 'utf8'),
signUpWithVerificationCodeMutation: readFileSync(gqlDir + '/SignupWithVerificationCodeMutation.graphql', 'utf8'),
sendVerificationCodeMutation: readFileSync(gqlDir + '/SendVerificationCodeForLoginMutation.graphql', 'utf8')
}
const optionMap = [
{ title: 'Claude (Powered by Anthropic)', value: 'a2' },
{ title: 'Sage (Powered by OpenAI - logical)', value: 'capybara' },
{ title: 'Dragonfly (Powered by OpenAI - simpler)', value: 'nutria' },
{ title: 'ChatGPT (Powered by OpenAI - current)', value: 'chinchilla' },
{ title: 'Claude+', value: 'a2_2' },
{ title: 'GPT-4', value: 'beaver' }
]
export class PoeClient {
constructor (props) {
this.config = props
}
headers = {
'Content-Type': 'application/json',
Referrer: 'https://poe.com/',
Origin: 'https://poe.com',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
}
chatId = 0
bot = ''
reConnectWs = false
async setCredentials () {
let result = await scrape(this.config.quora_cookie, this.config.proxy ? proxy(Config.proxy) : null)
console.log(result)
this.config.quora_formkey = result.appSettings.formkey
this.config.channel_name = result.channelName
this.config.app_settings = result.appSettings
// set value
this.headers['poe-formkey'] = this.config.quora_formkey // unused
this.headers['poe-tchannel'] = this.config.channel_name
this.headers.Cookie = this.config.quora_cookie
console.log(this.headers)
}
async subscribe () {
const query = {
queryName: 'subscriptionsMutation',
variables: {
subscriptions: [
{
subscriptionName: 'messageAdded',
query: 'subscription subscriptions_messageAdded_Subscription(\n $chatId: BigInt!\n) {\n messageAdded(chatId: $chatId) {\n id\n messageId\n creationTime\n state\n ...ChatMessage_message\n ...chatHelpers_isBotMessage\n }\n}\n\nfragment ChatMessageDownvotedButton_message on Message {\n ...MessageFeedbackReasonModal_message\n ...MessageFeedbackOtherModal_message\n}\n\nfragment ChatMessageDropdownMenu_message on Message {\n id\n messageId\n vote\n text\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageFeedbackButtons_message on Message {\n id\n messageId\n vote\n voteReason\n ...ChatMessageDownvotedButton_message\n}\n\nfragment ChatMessageOverflowButton_message on Message {\n text\n ...ChatMessageDropdownMenu_message\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageSuggestedReplies_SuggestedReplyButton_message on Message {\n messageId\n}\n\nfragment ChatMessageSuggestedReplies_message on Message {\n suggestedReplies\n ...ChatMessageSuggestedReplies_SuggestedReplyButton_message\n}\n\nfragment ChatMessage_message on Message {\n id\n messageId\n text\n author\n linkifiedText\n state\n ...ChatMessageSuggestedReplies_message\n ...ChatMessageFeedbackButtons_message\n ...ChatMessageOverflowButton_message\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isBotMessage\n ...chatHelpers_isChatBreak\n ...chatHelpers_useTimeoutLevel\n ...MarkdownLinkInner_message\n}\n\nfragment MarkdownLinkInner_message on Message {\n messageId\n}\n\nfragment MessageFeedbackOtherModal_message on Message {\n id\n messageId\n}\n\nfragment MessageFeedbackReasonModal_message on Message {\n id\n messageId\n}\n\nfragment chatHelpers_isBotMessage on Message {\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isChatBreak\n}\n\nfragment chatHelpers_isChatBreak on Message {\n author\n}\n\nfragment chatHelpers_isHumanMessage on Message {\n author\n}\n\nfragment chatHelpers_useTimeoutLevel on Message {\n id\n state\n text\n messageId\n}\n'
},
{
subscriptionName: 'viewerStateUpdated',
query: 'subscription subscriptions_viewerStateUpdated_Subscription {\n viewerStateUpdated {\n id\n ...ChatPageBotSwitcher_viewer\n }\n}\n\nfragment BotHeader_bot on Bot {\n displayName\n ...BotImage_bot\n}\n\nfragment BotImage_bot on Bot {\n profilePicture\n displayName\n}\n\nfragment BotLink_bot on Bot {\n displayName\n}\n\nfragment ChatPageBotSwitcher_viewer on Viewer {\n availableBots {\n id\n ...BotLink_bot\n ...BotHeader_bot\n }\n}\n'
}
]
},
query: 'mutation subscriptionsMutation(\n $subscriptions: [AutoSubscriptionQuery!]!\n) {\n autoSubscribe(subscriptions: $subscriptions) {\n viewer {\n id\n }\n }\n}\n'
}
await this.makeRequest(query)
}
async makeRequest (request) {
let payload = JSON.stringify(request)
let baseString = payload + this.headers['poe-formkey'] + 'WpuLMiXEKKE98j56k'
const md5 = crypto.createHash('md5').update(baseString).digest('hex')
let option = {
method: 'POST',
headers: Object.assign(this.headers, {
'poe-tag-id': md5,
'content-type': 'application/json'
}),
body: payload
}
if (this.config.proxy) {
option.agent = proxy(Config.proxy)
}
const response = await fetch('https://poe.com/api/gql_POST', option)
let text = await response.text()
try {
let result = JSON.parse(text)
console.log({ result })
return result
} catch (e) {
console.error(text)
throw e
}
}
async getBot (displayName) {
let r
let retry = 10
while (retry >= 0) {
let url = `https://poe.com/_next/data/${this.nextData.buildId}/${displayName}.json`
let option = {
headers: this.headers
}
if (this.config.proxy) {
option.agent = proxy(Config.proxy)
}
let r = await fetch(url, option)
let res = await r.text()
try {
let chatData = (JSON.parse(res)).pageProps.payload.chatOfBotDisplayName
return chatData
} catch (e) {
r = res
retry--
}
}
throw new Error(r)
}
async getChatId () {
let option = {
headers: this.headers
}
if (this.config.proxy) {
option.agent = proxy(Config.proxy)
}
let r = await fetch('https://poe.com', option)
let text = await r.text()
const jsonRegex = /<script id="__NEXT_DATA__" type="application\/json">(.+?)<\/script>/
const jsonText = text.match(jsonRegex)[1]
const nextData = JSON.parse(jsonText)
this.nextData = nextData
this.viewer = nextData.props.pageProps.payload.viewer
this.formkey = this.extract_formkey(text)
this.headers['poe-formkey'] = this.formkey
let bots = this.viewer.availableBots
this.bots = {}
for (let i = 0; i < bots.length; i++) {
let bot = bots[i]
let chatData = await this.getBot(bot.displayName)
this.bots[chatData.defaultBotObject.nickname] = chatData
}
console.log(this.bots)
}
extract_formkey (html) {
const scriptRegex = /<script>if\(.+\)throw new Error;(.+)<\/script>/
const scriptText = html.match(scriptRegex)[1]
const keyRegex = /var .="([0-9a-f]+)",/
const keyText = scriptText.match(keyRegex)[1]
const cipherRegex = /.\[(\d+)]=.\[(\d+)]/g
const cipherPairs = scriptText.match(cipherRegex)
const formkeyList = Array(cipherPairs.length).fill('')
for (const pair of cipherPairs) {
const [formkeyIndex, keyIndex] = pair.match(/\d+/g).map(Number)
formkeyList[formkeyIndex] = keyText[keyIndex]
}
const formkey = formkeyList.join('')
return formkey
}
async clearContext (bot) {
try {
const data = await this.makeRequest({
query: `${queries.addMessageBreakMutation}`,
variables: { chatId: this.config.chat_ids[bot] }
})
if (!data.data) {
this.reConnectWs = true // for websocket purpose
console.log('ON TRY! Could not clear context! Trying to reLogin..')
}
return data
} catch (e) {
this.reConnectWs = true // for websocket purpose
console.log('ON CATCH! Could not clear context! Trying to reLogin..')
return e
}
}
async sendMsg (bot, query) {
try {
const data = await this.makeRequest({
query: `${queries.addHumanMessageMutation}`,
variables: {
bot,
chatId: this.bots[bot].chatId,
query,
source: null,
withChatBreak: false
}
})
console.log(data)
if (!data.data) {
this.reConnectWs = true // for cli websocket purpose
console.log('Could not send message! Trying to reLogin..')
}
return data
} catch (e) {
this.reConnectWs = true // for cli websocket purpose
console.error(e)
return e
}
}
async getHistory (bot) {
try {
let response = await this.makeRequest({
query: `${queries.chatPaginationQuery}`,
variables: {
before: null,
bot,
last: 25
}
})
return response.data.chatOfBot.messagesConnection.edges
.map(({ node: { messageId, text, authorNickname } }) => ({
messageId,
text,
authorNickname
}))
} catch (e) {
console.log('There has been an error while fetching your history!')
}
}
async deleteMessages (msgIds) {
await this.makeRequest({
queryName: 'MessageDeleteConfirmationModal_deleteMessageMutation_Mutation',
variables: {
messageIds: msgIds
},
query: 'mutation MessageDeleteConfirmationModal_deleteMessageMutation_Mutation(\n $messageIds: [BigInt!]!\n){\n messagesDelete(messageIds: $messageIds) {\n edgeIds\n }\n}\n'
})
}
async getResponse (bot) {
let text
let state
let authorNickname
try {
while (true) {
await new Promise((resolve) => setTimeout(resolve, 2000))
let response = await this.makeRequest({
query: `${queries.chatPaginationQuery}`,
variables: {
before: null,
bot,
last: 1
}
})
let base = response.data.chatOfBot.messagesConnection.edges
let lastEdgeIndex = base.length - 1
text = base[lastEdgeIndex].node.text
authorNickname = base[lastEdgeIndex].node.authorNickname
state = base[lastEdgeIndex].node.state
if (state === 'complete' && authorNickname === bot) {
break
}
}
} catch (e) {
console.log('Could not get response!')
return {
status: false,
message: 'failed',
data: null
}
}
return {
status: true,
message: 'success',
data: text
}
}
}

View file

@ -1,65 +0,0 @@
import WebSocket from 'ws'
import * as diff from 'diff'
import { readFileSync } from 'fs'
const getSocketUrl = async () => {
const tchRand = Math.floor(100000 + Math.random() * 900000) // They're surely using 6 digit random number for ws url.
const socketUrl = `wss://tch${tchRand}.tch.quora.com`
const credentials = JSON.parse(readFileSync('config.json', 'utf8'))
const appSettings = credentials.app_settings.tchannelData
const boxName = appSettings.boxName
const minSeq = appSettings.minSeq
const channel = appSettings.channel
const hash = appSettings.channelHash
return `${socketUrl}/up/${boxName}/updates?min_seq=${minSeq}&channel=${channel}&hash=${hash}`
}
export const connectWs = async () => {
const url = await getSocketUrl()
const ws = new WebSocket(url)
return new Promise((resolve, reject) => {
ws.on('open', function open () {
console.log('Connected to websocket')
return resolve(ws)
})
})
}
export const disconnectWs = async (ws) => {
return new Promise((resolve, reject) => {
ws.on('close', function close () {
return resolve(true)
})
ws.close()
})
}
export const listenWs = async (ws) => {
let previousText = ''
return new Promise((resolve, reject) => {
const onMessage = function incoming (data) {
let jsonData = JSON.parse(data)
if (jsonData.messages && jsonData.messages.length > 0) {
const messages = JSON.parse(jsonData.messages[0])
const dataPayload = messages.payload.data
const text = dataPayload.messageAdded.text
const state = dataPayload.messageAdded.state
if (state !== 'complete') {
const differences = diff.diffChars(previousText, text)
let result = ''
differences.forEach((part) => {
if (part.added) {
result += part.value
}
})
previousText = text
process.stdout.write(result)
} else {
ws.removeListener('message', onMessage)
return resolve(true)
}
}
}
ws.on('message', onMessage)
})
}

View file

@ -1,65 +0,0 @@
import WebSocket from 'ws'
import * as diff from 'diff'
import { readFileSync } from 'fs'
const getSocketUrl = async () => {
const tchRand = Math.floor(100000 + Math.random() * 900000) // They're surely using 6 digit random number for ws url.
const socketUrl = `wss://tch${tchRand}.tch.quora.com`
const credentials = JSON.parse(readFileSync('config.json', 'utf8'))
const appSettings = credentials.app_settings.tchannelData
const boxName = appSettings.boxName
const minSeq = appSettings.minSeq
const channel = appSettings.channel
const hash = appSettings.channelHash
return `${socketUrl}/up/${boxName}/updates?min_seq=${minSeq}&channel=${channel}&hash=${hash}`
}
export const connectWs = async () => {
const url = await getSocketUrl()
const ws = new WebSocket(url)
return new Promise((resolve, reject) => {
ws.on('open', function open () {
console.log('Connected to websocket')
return resolve(ws)
})
})
}
export const disconnectWs = async (ws) => {
return new Promise((resolve, reject) => {
ws.on('close', function close () {
return resolve(true)
})
ws.close()
})
}
export const listenWs = async (ws) => {
let previousText = ''
return new Promise((resolve, reject) => {
const onMessage = function incoming (data) {
let jsonData = JSON.parse(data)
if (jsonData.messages && jsonData.messages.length > 0) {
const messages = JSON.parse(jsonData.messages[0])
const dataPayload = messages.payload.data
const text = dataPayload.messageAdded.text
const state = dataPayload.messageAdded.state
if (state !== 'complete') {
const differences = diff.diffChars(previousText, text)
let result = ''
differences.forEach((part) => {
if (part.added) {
result += part.value
}
})
previousText = text
process.stdout.write(result)
} else {
ws.removeListener('message', onMessage)
return resolve(true)
}
}
}
ws.on('message', onMessage)
})
}

View file

@ -1,170 +0,0 @@
import { Config } from '../config.js'
import slack from '@slack/bolt'
import { limitString } from '../common.js'
import common from '../../../../lib/common/common.js'
let proxy
if (Config.proxy) {
try {
proxy = (await import('https-proxy-agent')).default
} catch (e) {
console.warn('未安装https-proxy-agent请在插件目录下执行pnpm add https-proxy-agent')
}
}
export class SlackClaudeClient {
constructor (props) {
this.config = props
if (Config.slackSigningSecret && Config.slackBotUserToken && Config.slackUserToken) {
let option = {
signingSecret: Config.slackSigningSecret,
token: Config.slackBotUserToken,
// socketMode: true,
appToken: Config.slackUserToken
// port: 45912
}
if (Config.proxy) {
option.agent = proxy(Config.proxy)
}
option.logLevel = Config.debug ? 'debug' : 'info'
this.app = new slack.App(option)
} else {
throw new Error('未配置Slack信息')
}
}
async sendMessage (prompt, e, t = 0) {
if (t > 10) {
return 'claude 未响应'
}
if (prompt.length > 3990) {
logger.warn('消息长度大于slack限制长度剪切至3990')
prompt = limitString(prompt, 3990, false)
}
let channel
let qq = e.sender.user_id
if (Config.slackClaudeSpecifiedChannel) {
channel = { id: Config.slackClaudeSpecifiedChannel }
} else {
let channels = await this.app.client.conversations.list({
token: this.config.slackUserToken,
types: 'public_channel,private_channel'
})
channel = channels.channels.filter(c => c.name === '' + qq)
if (!channel || channel.length === 0) {
let createChannelResponse = await this.app.client.conversations.create({
token: this.config.slackUserToken,
name: qq + '',
is_private: true
})
channel = createChannelResponse.channel
await this.app.client.conversations.invite({
token: this.config.slackUserToken,
channel: channel.id,
users: Config.slackClaudeUserId
})
await common.sleep(1000)
} else {
channel = channel[0]
}
}
let conversationId = await redis.get(`CHATGPT:SLACK_CONVERSATION:${qq}`)
if (!conversationId) {
let sendResponse = await this.app.client.chat.postMessage({
as_user: true,
text: `<@${Config.slackClaudeUserId}> ${prompt}`,
token: this.config.slackUserToken,
channel: channel.id
})
let ts = sendResponse.ts
let response = '_Typing…_'
let tryTimes = 0
// 发完先等3喵
await common.sleep(3000)
while (response.trim().endsWith('_Typing…_')) {
let replies = await this.app.client.conversations.replies({
token: this.config.slackUserToken,
channel: channel.id,
limit: 1000,
ts
})
await await redis.set(`CHATGPT:SLACK_CONVERSATION:${qq}`, `${ts}`)
if (replies.messages.length > 0) {
let formalMessages = replies.messages
.filter(m => m.metadata?.event_type !== 'claude_moderation')
.filter(m => !m.text.startsWith('_'))
if (!formalMessages[formalMessages.length - 1].bot_profile) {
// 问题的下一句不是bot回复的这属于意料之外的问题可能是多人同时问问题导致 再问一次吧
return await this.sendMessage(prompt, e, t + 1)
}
let reply = formalMessages[formalMessages.length - 1]
if (!reply.text.startsWith(`<@${Config.slackClaudeUserId}>`)) {
response = reply.text
if (Config.debug) {
let text = response.replace('_Typing…_', '')
if (text) {
logger.info(response.replace('_Typing…_', ''))
}
}
}
}
await common.sleep(2000)
tryTimes++
if (tryTimes > 3 && response === '_Typing…_') {
// 过了6秒还没任何回复就重新发一下试试
logger.warn('claude没有响应重试中')
return await this.sendMessage(prompt, e, t + 1)
}
}
return response
} else {
let postResponse = await this.app.client.chat.postMessage({
as_user: true,
text: `<@${Config.slackClaudeUserId}> ${prompt}`,
token: this.config.slackUserToken,
channel: channel.id,
thread_ts: conversationId
})
let postTs = postResponse.ts
let response = '_Typing…_'
let tryTimes = 0
// 发完先等3喵
await common.sleep(3000)
while (response.trim().endsWith('_Typing…_')) {
let replies = await this.app.client.conversations.replies({
token: this.config.slackUserToken,
channel: channel.id,
limit: 1000,
ts: conversationId,
oldest: postTs
})
if (replies.messages.length > 0) {
let formalMessages = replies.messages
.filter(m => m.metadata?.event_type !== 'claude_moderation')
.filter(m => !m.text.startsWith('_'))
if (!formalMessages[formalMessages.length - 1].bot_profile) {
// 问题的下一句不是bot回复的这属于意料之外的问题可能是多人同时问问题导致 再问一次吧
return await this.sendMessage(prompt, e, t + 1)
}
let reply = formalMessages[formalMessages.length - 1]
if (!reply.text.startsWith(`<@${Config.slackClaudeUserId}>`)) {
response = reply.text
if (Config.debug) {
let text = response.replace('_Typing…_', '')
if (text) {
logger.info(response.replace('_Typing…_', ''))
}
}
}
}
await common.sleep(2000)
tryTimes++
if (tryTimes > 3 && response === '_Typing…_') {
// 过了6秒还没任何回复就重新发一下试试
logger.warn('claude没有响应重试中')
return await this.sendMessage(prompt, e, t + 1)
}
}
return response
}
}
}

View file

@ -21,6 +21,7 @@ export class WebsiteTool extends AbstractTool {
func = async function (opts) { func = async function (opts) {
let { url, mode, e } = opts let { url, mode, e } = opts
let browser
try { try {
// let res = await fetch(url, { // let res = await fetch(url, {
// headers: { // headers: {
@ -34,7 +35,7 @@ export class WebsiteTool extends AbstractTool {
origin = true origin = true
} }
let ppt = new ChatGPTPuppeteer() let ppt = new ChatGPTPuppeteer()
let browser = await ppt.getBrowser() browser = await ppt.getBrowser()
let page = await browser.newPage() let page = await browser.newPage()
await page.goto(url, { await page.goto(url, {
waitUntil: 'networkidle2' waitUntil: 'networkidle2'
@ -104,6 +105,12 @@ export class WebsiteTool extends AbstractTool {
} }
} catch (err) { } catch (err) {
return `failed to visit the website, error: ${err.toString()}` return `failed to visit the website, error: ${err.toString()}`
} finally {
if (browser) {
try {
await browser.close()
} catch (err) {}
}
} }
} }

View file

@ -1,28 +1,5 @@
import { Config } from '../config.js' import { Config } from '../config.js'
import { newFetch } from '../proxy.js'
let proxy
if (Config.proxy) {
try {
proxy = (await import('https-proxy-agent')).default
} catch (e) {
console.warn('未安装https-proxy-agent请在插件目录下执行pnpm add https-proxy-agent')
}
}
const newFetch = (url, options = {}) => {
const defaultOptions = Config.proxy
? {
agent: proxy(Config.proxy)
}
: {}
const mergedOptions = {
...defaultOptions,
...options
}
return fetch(url, mergedOptions)
}
/** /**
* 生成voxTTSMode下的wav音频 * 生成voxTTSMode下的wav音频

View file

@ -1,15 +1,8 @@
import { Config } from '../config.js' import { Config } from '../config.js'
import fs from 'fs' import fs from 'fs'
import nodejieba from '@node-rs/jieba'
let nodejieba class Tokenizer {
try {
nodejieba = (await import('@node-rs/jieba')).default
nodejieba.load()
} catch (err) {
logger.info('未安装@node-rs/jieba娱乐功能-词云统计不可用')
}
export class Tokenizer {
async getHistory (e, groupId, date = new Date(), duration = 0, userId) { async getHistory (e, groupId, date = new Date(), duration = 0, userId) {
if (!groupId) { if (!groupId) {
throw new Error('no valid group id') throw new Error('no valid group id')
@ -78,6 +71,10 @@ export class Tokenizer {
if (!nodejieba) { if (!nodejieba) {
throw new Error('未安装node-rs/jieba娱乐功能-词云统计不可用') throw new Error('未安装node-rs/jieba娱乐功能-词云统计不可用')
} }
if (!this.loaded) {
nodejieba.load()
this.loaded = true
}
// duration represents the number of hours to go back, should in range [0, 24] // duration represents the number of hours to go back, should in range [0, 24]
let chats = await this.getHistory(e, groupId, new Date(), duration, userId) let chats = await this.getHistory(e, groupId, new Date(), duration, userId)
let durationStr = duration > 0 ? `${duration}小时` : '今日' let durationStr = duration > 0 ? `${duration}小时` : '今日'
@ -139,7 +136,7 @@ export class Tokenizer {
} }
} }
export class ShamrockTokenizer extends Tokenizer { class ShamrockTokenizer extends Tokenizer {
async getHistory (e, groupId, date = new Date(), duration = 0, userId) { async getHistory (e, groupId, date = new Date(), duration = 0, userId) {
logger.mark('当前使用Shamrock适配器') logger.mark('当前使用Shamrock适配器')
if (!groupId) { if (!groupId) {
@ -227,3 +224,8 @@ function isTimestampInDateRange (timestamp, startOfSpecifiedDate, endOfSpecified
// Step 5: Compare the given timestamp with the start and end of the specified date // Step 5: Compare the given timestamp with the start and end of the specified date
return timestamp >= startOfSpecifiedDate && timestamp < endOfSpecifiedDate return timestamp >= startOfSpecifiedDate && timestamp < endOfSpecifiedDate
} }
export default {
default: new Tokenizer(),
shamrock: new ShamrockTokenizer()
}

View file

@ -1,4 +1,4 @@
import { ShamrockTokenizer, Tokenizer } from './tokenizer.js' import Tokenizer from './tokenizer.js'
import { render } from '../common.js' import { render } from '../common.js'
export async function makeWordcloud (e, groupId, duration = 0, userId) { export async function makeWordcloud (e, groupId, duration = 0, userId) {
@ -12,8 +12,8 @@ export async function makeWordcloud (e, groupId, duration = 0, userId) {
function getTokenizer (e) { function getTokenizer (e) {
if (e.adapter === 'shamrock') { if (e.adapter === 'shamrock') {
return new ShamrockTokenizer() return Tokenizer.shamrock
} else { } else {
return new Tokenizer() return Tokenizer.default
} }
} }