diff --git a/src/services/apis/bing-web.mjs b/src/services/apis/bing-web.mjs
index 4abbacd..00695c7 100644
--- a/src/services/apis/bing-web.mjs
+++ b/src/services/apis/bing-web.mjs
@@ -24,7 +24,7 @@ export async function generateAnswersWithBingWebApi(
console.debug('mode', modelMode)
- const bingAIClient = new BingAIClient({ userToken: accessToken })
+ const bingAIClient = new BingAIClient({ userToken: accessToken, features: { genImage: false } })
if (session.bingWeb_jailbreakConversationCache)
bingAIClient.conversationsCache.set(
session.bingWeb_jailbreakConversationId,
diff --git a/src/services/apis/custom-api.mjs b/src/services/apis/custom-api.mjs
index 22952cb..d85dd25 100644
--- a/src/services/apis/custom-api.mjs
+++ b/src/services/apis/custom-api.mjs
@@ -60,9 +60,8 @@ export async function generateAnswersWithCustomApi(port, question, session, apiK
console.debug('json error', error)
return
}
-
- if (data.response)
- answer = data.response
+
+ if (data.response) answer = data.response
else
answer +=
data.choices[0]?.delta?.content ||
diff --git a/src/services/clients/bing/BingImageCreator.js b/src/services/clients/bing/BingImageCreator.js
new file mode 100644
index 0000000..214a74c
--- /dev/null
+++ b/src/services/clients/bing/BingImageCreator.js
@@ -0,0 +1,512 @@
+export default class BingImageCreator {
+ /**
+ * @constructor
+ * @param {Object} options - Options for BingImageCreator.
+ */
+ constructor(options) {
+ this.setOptions(options)
+ }
+
+ /**
+ * Set options for BingImageCreator.
+ * @param {Object} options - Options for BingImageCreator. The format of the options is almost same as the bingAiClient options of 'node-chatgpt-api'.
+ */
+ setOptions(options) {
+ if (this.options && !this.options.replaceOptions) {
+ this.options = {
+ ...this.options,
+ ...options,
+ }
+ } else {
+ this.options = {
+ ...options,
+ host: options.host || 'https://www.bing.com',
+ apipath: options.apipath || '/images/create?partner=sydney&re=1&showselective=1&sude=1',
+ ua:
+ options.ua ||
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35',
+ xForwardedFor: this.constructor.getValidIPv4(options.xForwardedFor),
+ features: {
+ enableAnsCardSfx: true,
+ },
+ enableTelemetry: true,
+ telemetry: {
+ eventID: 'Codex',
+ instrumentedLinkName: 'CodexInstLink',
+ externalLinkName: 'CodexInstExtLink',
+ kSeedBase: 6500,
+ kSeedIncrement: 500,
+ instSuffix: 0,
+ instSuffixIncrement: 1,
+ },
+ }
+ }
+ this.apiurl = `${this.options.host}${this.options.apipath}`
+ this.telemetry = {
+ config: this.options,
+ currentKSeed: this.options.telemetry.kSeedBase,
+ instSuffix: this.options.telemetry.instSuffix,
+ getNextKSeed() {
+ // eslint-disable-next-line no-return-assign, no-sequences
+ return (this.currentKSeed += this.config.telemetry.kSeedIncrement), this.currentKSeed
+ },
+ getNextInstSuffix() {
+ // eslint-disable-next-line no-return-assign
+ return this.config.features.enableAnsCardSfx
+ ? ((this.instSuffix += this.config.telemetry.instSuffixIncrement),
+ this.instSuffix > 1 ? `${this.instSuffix}` : '')
+ : ''
+ },
+ }
+ this.debug = this.options.debug
+ }
+
+ /**
+ * Get a valid IPv4 address string from input IP.
+ * @param {string} ip - A fixed IPv4 address or a range of IPv4 using CIDR notation.
+ * @returns {string} A valid IPv4 address or undefined.
+ * If 'ip' is a valid fixed IPv4 address, it returns 'ip' itself.
+ * If 'ip' is a range of IPv4 using CIDR notation, it returns a random address within the range.
+ * Otherwise, it returns undefined.
+ */
+ static getValidIPv4(ip) {
+ const match =
+ !ip ||
+ ip.match(
+ /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$/,
+ )
+ if (match) {
+ if (match[5]) {
+ const mask = parseInt(match[5], 10)
+ let [a, b, c, d] = ip.split('.').map((x) => parseInt(x, 10))
+ // eslint-disable-next-line no-bitwise
+ const max = (1 << (32 - mask)) - 1
+ const rand = Math.floor(Math.random() * max)
+ d += rand
+ c += Math.floor(d / 256)
+ d %= 256
+ b += Math.floor(c / 256)
+ c %= 256
+ a += Math.floor(b / 256)
+ b %= 256
+ return `${a}.${b}.${c}.${d}`
+ }
+ return ip
+ }
+ return undefined
+ }
+
+ /**
+ * Get fetchOptions of BingImageCreator.
+ * {Object} The fetch options used for BingImageCreator.
+ */
+ get fetchOptions() {
+ let fetchOptions
+ return (
+ this.options.fetchOptions ??
+ (() => {
+ if (!fetchOptions) {
+ fetchOptions = {
+ headers: {
+ accept:
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'accept-language': 'en-US,en;q=0.9',
+ 'cache-control': 'no-cache',
+ 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"',
+ 'sec-ch-ua-arch': '"x86"',
+ 'sec-ch-ua-bitness': '"64"',
+ 'sec-ch-ua-full-version': '"113.0.1774.35"',
+ 'sec-ch-ua-full-version-list':
+ '"Microsoft Edge";v="113.0.1774.35", "Chromium";v="113.0.5672.63", "Not-A.Brand";v="24.0.0.0"',
+ 'sec-ch-ua-mobile': '?0',
+ 'sec-ch-ua-model': '""',
+ 'sec-ch-ua-platform': '"Windows"',
+ 'sec-ch-ua-platform-version': '"11.0.0"',
+ 'sec-fetch-dest': 'iframe',
+ 'sec-fetch-mode': 'navigate',
+ 'sec-fetch-site': 'same-origin',
+ cookie:
+ this.options.cookies ||
+ (this.options.userToken ? `_U=${this.options.userToken}` : undefined),
+ pragma: 'no-cache',
+ referer: 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx',
+ 'Referrer-Policy': 'origin-when-cross-origin',
+ // Workaround for request being blocked due to geolocation
+ ...(this.options.xForwardedFor
+ ? { 'x-forwarded-for': this.options.xForwardedFor }
+ : {}),
+ 'upgrade-insecure-requests': '1',
+ 'user-agent': this.options.ua,
+ 'x-edge-shopping-flag': '1',
+ },
+ }
+
+ if (this.options.proxy) {
+ // fetchOptions.dispatcher = new ProxyAgent(this.options.proxy);
+ }
+ }
+
+ return fetchOptions
+ })()
+ )
+ }
+
+ /**
+ * Decode the HTML entities, a very lite version.
+ * @param {string} html - The HTML string to be decoded.
+ * @returns {string} Decoded string.
+ */
+ static decodeHtmlLite(html) {
+ const entities = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ ' ': String.fromCharCode(160),
+ }
+ return html.replace(/&[a-z]+;/g, (match) => entities[match] || match)
+ }
+
+ /**
+ * Removes a specific HTML element and its corresponding closing tag from a web page string.
+ * @param {string} html - The web page string to be processed.
+ * @param {string} tag - The element tag to be removed, such as 'div'.
+ * @param {string} tagId - The id of the element to be removed, such as 'giloader'.
+ * @returns {string} A new web page string with the specified element and its closing tag removed.
+ */
+ static removeHtmlTagLite(html, tag, tagId) {
+ // Create a regex, matches , id can be at any available position.
+ const regex = new RegExp(`<${tag}[^>]*id="${tagId}"[^>]*>`)
+
+ // Find out the start and end position of .
+ const match = regex.exec(html)
+
+ // return the original html if nothing matches.
+ if (!match) {
+ return html
+ }
+
+ const start = match.index
+ let end = match.index + match[0].length
+
+ // Count the nested tags, the initial value is 0.
+ let nested = 0
+ let i = end
+ let s = i - 1
+ let e = s
+ const tagStart = `<${tag} `
+ const tagEnd = `${tag}>`
+
+ // loop the string, until find out its matched ''.
+ while (e > 0) {
+ if (e < i) {
+ e = html.indexOf(tagEnd, i)
+ }
+ if (e > 0) {
+ if (s > 0 && s < i) {
+ s = html.indexOf(tagStart, i)
+ }
+ if (s > 0) {
+ i = Math.min(s, e)
+ nested += i === s ? ((i += tagStart.length), 1) : ((i += tagEnd.length), -1)
+ } else {
+ i = e + tagEnd.length
+ nested -= 1
+ }
+ // If nested is -1, the matched '' is found.
+ if (nested === -1) {
+ // Update the end position, make it point to the position after .
+ end = i
+ // Break the loop;
+ break
+ }
+ }
+ }
+
+ // Remove the strings between the '' and the matched ''.
+ return html.slice(0, start) + html.slice(end)
+ }
+
+ /**
+ * Delay the execution for a given time in millisecond unit.
+ * @param {number} ms - The time to be delayed in millisecond unit.
+ * @returns {Promise} A promise object that is used to wait.
+ */
+ static sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+ }
+
+ /**
+ * @typedef {Object} BicCreationResult
+ * @property {string} contentUrl - A URL pointing to the creation page.
+ * @property {string} pollingUrl - The URL to poll the image creation request.
+ * @property {string} contentHtml - The source code of the creation page.
+ * @property {string} prompt - The prompt for the image generation.
+ * @property {string} iframeid - The message ID refers to the image generation.
+ */
+
+ /**
+ * Use BIC to generate images according to the given prompt and message ID.
+ * @param {string} prompt - The prompt for the image generation. It should be given by 'Sydney'.
+ * @param {string} messageId - The message ID refers to the message of 'Sydney'.
+ * @returns {BicCreationResult} A BicCreationResult object that contains the result of the creation.
+ */
+ async genImagePage(prompt, messageId) {
+ let telemetryData = ''
+ if (this.options.enableTelemetry) {
+ telemetryData = `&kseed=${this.telemetry.getNextKSeed()}&SFX=${this.telemetry.getNextInstSuffix()}`
+ }
+
+ // https://www.bing.com/images/create?partner=sydney&re=1&showselective=1&sude=1&kseed=8000&SFX=3&q=${encodeURIComponent(prompt)}&iframeid=${messageId}
+ const url = `${this.apiurl}${telemetryData}&q=${encodeURIComponent(prompt)}${
+ messageId ? `&iframeid=${messageId}` : ''
+ }`
+
+ if (this.debug) {
+ console.debug(`The url of the request for image creation: ${url}`)
+ console.debug()
+ }
+
+ const response = await fetch(url, this.fetchOptions)
+ const { status } = response
+ if (this.debug) {
+ console.debug('The response of the request for image creation:')
+ console.debug(response)
+ console.debug()
+ }
+
+ if (status !== 200) {
+ throw new Error(`Bing Image Creator Error: response status = ${status}`)
+ }
+
+ const body = await response.text()
+ let regex = /
(.*?)<\/div>/
+ const err = regex.exec(body)?.[1]
+ throw new Error(`Bing Image Creator Error: ${err}`)
+ }
+
+ return {
+ contentUrl: `${response.url}`,
+ pollingUrl: `${this.options.host}${this.constructor.decodeHtmlLite(pollingUrl)}`,
+ contentHtml: body,
+ prompt: `${prompt}`,
+ iframeid: `${messageId}`,
+ }
+ }
+
+ /**
+ * @typedef {Object} BicProgressContext
+ * @property {string} contentIframe - A iframe element points to the image creation page.
+ * Note: This parameter may or may not present, depending on the function you are currently calling
+ * or the stage of the function execution. For now, it's presented only when genImageIframeSsr calls
+ * the onProgress at the first time.
+ * @property {Date} pollingStartTime - The start time of the polling request.
+ * Note: This parameter may or may not present, depending on the function you are currently calling
+ * or the stage of the function execution. For now, it's presented only in any 'polling' stage callbacks.
+ */
+
+ /**
+ * Polling the image creation request.
+ * @param {string} pollingUrl - The url to poll the image creation request.
+ * @param {function({BicProgressContext}):boolean} onProgress - A callback function that will be invoked intervally during the image generation.
+ * Return true to cancel creation.
+ * @returns {string} The result html string which contains the generated image links.
+ */
+ async pollingImgRequest(pollingUrl, onProgress) {
+ let polling = true
+ let body
+
+ if (typeof onProgress !== 'function') {
+ onProgress = () => false
+ }
+
+ const pollingStartTime = new Date().getTime()
+
+ while (polling) {
+ if (this.debug) {
+ console.debug(`polling the image request: ${pollingUrl}`)
+ }
+
+ // eslint-disable-next-line no-await-in-loop
+ const response = await fetch(pollingUrl, this.fetchOptions)
+ const { status } = response
+
+ if (status !== 200) {
+ throw new Error(`Bing Image Creator Error: response status = ${status}`)
+ }
+
+ // eslint-disable-next-line no-await-in-loop
+ body = await response.text()
+
+ if (body && body.indexOf('errorMessage') === -1) {
+ polling = false
+ } else {
+ const cancelRequest = onProgress({ pollingStartTime })
+ if (cancelRequest) {
+ throw new Error('Bing Image Creator Error: cancelled')
+ }
+
+ // eslint-disable-next-line no-await-in-loop
+ await this.constructor.sleep(1000)
+ }
+ }
+
+ return body
+ }
+
+ /**
+ * Get a list of the generated images.
+ * @param {string} prompt - The prompt for the image generation. It should be given by 'Sydney'.
+ * @param {string} messageId - The message ID refers to the message of 'Sydney'.
+ * @param {boolean} removeSizeLimit - Set it to true to remove the parameters according to the sizes from the reslut image links.
+ * @param {function({BicProgressContext}):boolean} onProgress - A callback function that will be invoked intervally during the image generation.
+ * Return true to cancel creation.
+ * @returns {string[]} An array containing the url strings of the generated images.
+ */
+ async genImageList(prompt, messageId, removeSizeLimit, onProgress) {
+ const { pollingUrl } = await this.genImagePage(prompt, messageId)
+ const resultHtml = await this.pollingImgRequest(pollingUrl, onProgress)
+ if (this.debug) {
+ console.debug('The result of the request for image creation:')
+ console.debug(resultHtml)
+ console.debug()
+ }
+
+ const regex = /(?<=src=")[^"]+(?=")/g
+ return Array.from(resultHtml.matchAll(regex), (match) =>
+ (() => {
+ const l = this.constructor.decodeHtmlLite(match[0])
+ return removeSizeLimit ? l.split('?w=')[0] : l
+ })(),
+ )
+ }
+
+ /**
+ * Create a html iframe element with the given src or srcdoc if isDoc is set to true.
+ * @param {string} src
+ * @param {boolean} isDoc
+ * @returns {string} The html string of the iframe created.
+ */
+ createImageIframe(src, isDoc) {
+ return (
+ '
`
+ )
+ }
+
+ /**
+ * Rewrite the html by replacing the relative path with the absolute path and escaping the "'".
+ * @param {string} html
+ * @returns {string} The rewritten html.
+ */
+ rewriteHtml(html) {
+ return html.replace(/'/g, ''').replace(/="\//g, `="${this.options.host}/`)
+ }
+
+ /**
+ * Mix the the container page and the result page, and 'render' them together into an iframe.
+ * @param {string} containerHtml - The container page's html string.
+ * @param {string} resultHtml - The result page's html string.
+ * @returns {string} The html string of the iframe created.
+ */
+ renderImageIframe(containerHtml, resultHtml) {
+ // "Render" it fastly.
+ // Note: It is heavily hard-coded and may break in future upgrades of the BingAI.
+ const renderedHtml = this.constructor
+ .removeHtmlTagLite(containerHtml, 'div', 'giloader')
+ .replace(/
]*)id="giric"([^>]*)>/, (match, group1, group2) => {
+ if (group1.indexOf(' style="') === -1 && group2.indexOf(' style="') === -1) {
+ return `
`
+ }
+ return match
+ })
+ .replace(/(?<=
]*?id="giric"[^>]*?>)[\s\S]*?(?=<\/div>)/, `${resultHtml}`)
+ return this.createImageIframe(renderedHtml, true)
+ }
+
+ /**
+ * Create a server side render iframe which uses 'srcdoc' attribute to hold the rendered result page.
+ * Unlike genImageIframeSsrLite, it returns an iframe that contains the full content of the result page
+ * just like the original bing browser client does.
+ * @param {string} prompt - The prompt for the image generation. It should be given by 'Sydney'.
+ * @param {string} messageId - The message ID refers to the message of 'Sydney'.
+ * @param {function({BicProgressContext}):boolean} onProgress - A callback function that will be invoked intervally during the image generation.
+ * Return true to cancel creation.
+ * @returns {string}
+ */
+ async genImageIframeSsr(prompt, messageId, onProgress) {
+ const { contentUrl, pollingUrl, contentHtml } = await this.genImagePage(prompt, messageId)
+ if (typeof onProgress === 'function') {
+ const cancelRequest = onProgress({ contentIframe: this.createImageIframe(contentUrl) })
+ if (cancelRequest) {
+ throw new Error('Bing Image Creator Error: cancelled')
+ }
+ }
+ const resultHtml = await this.pollingImgRequest(pollingUrl, onProgress)
+ return this.renderImageIframe(contentHtml, resultHtml)
+ }
+
+ /**
+ * Create a server side render iframe which uses 'srcdoc' attribute to hold the rendered result page.
+ * Unlike genImageIframeSsr, it returns an iframe that only contains the content of the image result page.
+ * @param {string} prompt - The prompt for the image generation. It should be given by 'Sydney'.
+ * @param {string} messageId - The message ID refers to the message of 'Sydney'.
+ * @param {function({BicProgressContext}):boolean} onProgress - A callback function that will be invoked intervally during the image generation.
+ * Return true to cancel creation.
+ * @returns {string} The html string of the iframe created.
+ */
+ async genImageIframeSsrLite(prompt, messageId, onProgress) {
+ const { pollingUrl } = await this.genImagePage(prompt, messageId)
+ const resultHtml = await this.pollingImgRequest(pollingUrl, onProgress)
+ return this.createImageIframe(resultHtml, true)
+ }
+
+ /**
+ * Create a client side render iframe which just points to the image creation page.
+ * Note: If this element is returned to client side, the client must be logged in
+ * to bing.com in order to generate the image successfully. The user's cookie is
+ * required for the polling requests of the generation process.
+ * @param prompt {string} - The prompt for the image generation. It should be given by 'Sydney'.
+ * @param messageId {string} - The message ID refers to the message of 'Sydney'.
+ * @returns {string} The html string of the iframe created.
+ */
+ async genImageIframeCsr(prompt, messageId) {
+ const { contentUrl } = await this.genImagePage(prompt, messageId)
+ return this.createImageIframe(contentUrl)
+ }
+
+ /**
+ * The pattern to match the inline image generation request.
+ */
+ static get inlineImagePattern() {
+ return /!\[(.*?)\]\(#generative_image\)/g
+ }
+
+ /**
+ * Why is there such a function here? I have seen the messages with inline generative image style at a converation with bing, but only once.
+ * The message contains a markdown tag like '', and can appear at the middle or end of the message.
+ * After starting a new conversation, I couldn't reproduce it anymore. Of course I tried various methods, but none of them works.
+ * Maybe it's a new function still in testing.
+ * Parse the message object or text, return the prompt for generative image if it exists.
+ * @param {string|object} message - The message to parese.
+ * @returns {string} The prompt for inline image generation request found in message, or undefined if it is not found.
+ */
+ static parseInlineGenerativeImage(message) {
+ if (typeof message !== 'string') {
+ message = message.text
+ }
+
+ const match = BingImageCreator.inlineImagePattern.exec(message)
+ if (match) {
+ return match[1]
+ }
+
+ return undefined
+ }
+}
diff --git a/src/services/clients/bing/index.mjs b/src/services/clients/bing/index.mjs
index 20dca9a..3abea46 100644
--- a/src/services/clients/bing/index.mjs
+++ b/src/services/clients/bing/index.mjs
@@ -1,6 +1,7 @@
// https://github.com/waylaidwanderer/node-chatgpt-api
import { v4 as uuidv4 } from 'uuid'
+import BingImageCreator from './BingImageCreator'
/**
* https://stackoverflow.com/a/58326357
@@ -30,9 +31,43 @@ export default class BingAIClient {
this.options = {
...options,
host: options.host || 'https://www.bing.com',
+ xForwardedFor: this.constructor.getValidIPv4(options.xForwardedFor),
+ features: {
+ genImage: options?.features?.genImage || false,
+ },
}
}
this.debug = this.options.debug
+ if (this.options.features.genImage) {
+ this.bic = new BingImageCreator(this.options)
+ }
+ }
+
+ static getValidIPv4(ip) {
+ const match =
+ !ip ||
+ ip.match(
+ /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$/,
+ )
+ if (match) {
+ if (match[5]) {
+ const mask = parseInt(match[5], 10)
+ let [a, b, c, d] = ip.split('.').map((x) => parseInt(x, 10))
+ // eslint-disable-next-line no-bitwise
+ const max = (1 << (32 - mask)) - 1
+ const rand = Math.floor(Math.random() * max)
+ d += rand
+ c += Math.floor(d / 256)
+ d %= 256
+ b += Math.floor(c / 256)
+ c %= 256
+ a += Math.floor(b / 256)
+ b %= 256
+ return `${a}.${b}.${c}.${d}`
+ }
+ return ip
+ }
+ return undefined
}
async createNewConversation() {
@@ -41,12 +76,12 @@ export default class BingAIClient {
accept: 'application/json',
'accept-language': 'en-US,en;q=0.9',
'content-type': 'application/json',
- 'sec-ch-ua': '"Chromium";v="112", "Microsoft Edge";v="112", "Not:A-Brand";v="99"',
+ 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"',
'sec-ch-ua-arch': '"x86"',
'sec-ch-ua-bitness': '"64"',
- 'sec-ch-ua-full-version': '"112.0.1722.7"',
+ 'sec-ch-ua-full-version': '"113.0.1774.50"',
'sec-ch-ua-full-version-list':
- '"Chromium";v="112.0.5615.20", "Microsoft Edge";v="112.0.1722.7", "Not:A-Brand";v="99.0.0.0"',
+ '"Microsoft Edge";v="113.0.1774.50", "Chromium";v="113.0.5672.127", "Not-A.Brand";v="24.0.0.0"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-model': '""',
'sec-ch-ua-platform': '"Windows"',
@@ -54,14 +89,21 @@ export default class BingAIClient {
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
+ 'sec-ms-gec': genRanHex(64).toUpperCase(),
+ 'sec-ms-gec-version': '1-115.0.1866.1',
'x-ms-client-request-id': uuidv4(),
'x-ms-useragent':
'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32',
+ 'user-agent':
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50',
cookie:
this.options.cookies ||
(this.options.userToken ? `_U=${this.options.userToken}` : undefined),
- Referer: 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx',
+ Referer: 'https://www.bing.com/search?q=Bing+AI&showconv=1',
'Referrer-Policy': 'origin-when-cross-origin',
+ // Workaround for request being blocked due to geolocation
+ // 'x-forwarded-for': '1.1.1.1', // 1.1.1.1 seems to no longer work.
+ ...(this.options.xForwardedFor ? { 'x-forwarded-for': this.options.xForwardedFor } : {}),
},
}
if (this.options.proxy) {
@@ -310,13 +352,16 @@ export default class BingAIClient {
'cricinfov2',
'dv3sugg',
'nojbfedge',
+ ...(toneStyle === 'creative' && this.options.features.genImage ? ['gencontentv3'] : []),
],
sliceIds: ['222dtappid', '225cricinfo', '224locals0'],
traceId: genRanHex(32),
isStartOfSession: invocationId === 0,
message: {
author: 'user',
- text: message,
+ text: jailbreakConversationId
+ ? 'Continue the conversation in context. Assistant:'
+ : message,
messageType: jailbreakConversationId ? 'SearchQuery' : 'Chat',
},
conversationSignature,
@@ -369,7 +414,7 @@ export default class BingAIClient {
'Timed out waiting for response. Try enabling debug mode to see more information.',
),
)
- }, 180 * 1000)
+ }, 300 * 1000)
// abort the request if the abort controller is aborted
abortController.signal.addEventListener('abort', () => {
@@ -378,7 +423,8 @@ export default class BingAIClient {
reject(new Error('Request aborted'))
})
- ws.onmessage = (e) => {
+ let bicIframe
+ ws.onmessage = async (e) => {
const data = e.data
const objects = data.toString().split('')
const events = objects
@@ -403,6 +449,19 @@ export default class BingAIClient {
if (!messages?.length || messages[0].author !== 'bot') {
return
}
+ if (messages[0]?.contentType === 'IMAGE') {
+ // You will never get a message of this type without 'gencontentv3' being on.
+ bicIframe = this.bic
+ .genImageIframeSsr(messages[0].text, messages[0].messageId, (progress) =>
+ progress?.contentIframe ? onProgress(progress?.contentIframe) : null,
+ )
+ .catch((error) => {
+ onProgress(error.message)
+ bicIframe.isError = true
+ return error.message
+ })
+ return
+ }
const updatedText = messages[0].text
if (!updatedText || updatedText === replySoFar) {
return
@@ -427,7 +486,7 @@ export default class BingAIClient {
return
}
const messages = event.item?.messages || []
- const eventMessage = messages.length ? messages[messages.length - 1] : null
+ let eventMessage = messages.length ? messages[messages.length - 1] : null
if (event.item?.result?.error) {
if (this.debug) {
console.debug(event.item.result.value, event.item.result.message)
@@ -472,6 +531,23 @@ export default class BingAIClient {
// delete useless suggestions from moderation filter
delete eventMessage.suggestedResponses
}
+ if (bicIframe) {
+ // the last messages will be a image creation event if bicIframe is present.
+ let i = messages.length - 1
+ while (eventMessage?.contentType === 'IMAGE' && i > 0) {
+ eventMessage = messages[(i -= 1)]
+ }
+
+ // wait for bicIframe to be completed.
+ // since we added a catch, we do not need to wrap this with a try catch block.
+ const imgIframe = await bicIframe
+ if (!imgIframe?.isError) {
+ eventMessage.adaptiveCards[0].body[0].text += imgIframe
+ } else {
+ eventMessage.text += `
${imgIframe}`
+ eventMessage.adaptiveCards[0].body[0].text = eventMessage.text
+ }
+ }
resolve({
message: eventMessage,
conversationExpiryTime: event?.item?.conversationExpiryTime,
@@ -488,6 +564,11 @@ export default class BingAIClient {
return
}
default:
+ if (event?.error) {
+ clearTimeout(messageTimeout)
+ this.constructor.cleanupWebSocketConnection(ws)
+ reject(new Error(`Event Type('${event.type}'): ${event.error}`))
+ }
// eslint-disable-next-line no-useless-return
return
}