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