// 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