mirror of
https://github.com/wassname/chatGPTBox.git
synced 2026-06-27 19:14:37 +08:00
update Bing client
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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 <tag id="tagId">, id can be at any available position.
|
||||
const regex = new RegExp(`<${tag}[^>]*id="${tagId}"[^>]*>`)
|
||||
|
||||
// Find out the start and end position of <tag id="tagId">.
|
||||
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 '</tag>'.
|
||||
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 '</tag>' is found.
|
||||
if (nested === -1) {
|
||||
// Update the end position, make it point to the position after </tag>.
|
||||
end = i
|
||||
// Break the loop;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the strings between the '<tag id="tagId">' and the matched '</tag>'.
|
||||
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 id="gir" data-c="([^"]*)"/
|
||||
const pollingUrl = regex.exec(body)?.[1]
|
||||
|
||||
if (!pollingUrl) {
|
||||
regex = /<div class="gil_err_mt">(.*?)<\/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 (
|
||||
'<iframe role="presentation" style="position:relative;overflow:hidden;width:475px;height:520px;' +
|
||||
'border:none;outline:none;padding:0px;margin:0px;display:flex;align-self:flex-start;border-radius:12px;' +
|
||||
'box-shadow:0px 0.3px 0.9px rgba(0, 0, 0, 0.12), 0px 1.6px 3.6px rgba(0, 0, 0, 0.16);z-index: 1;" ' +
|
||||
`${isDoc ? `srcdoc='${this.rewriteHtml(src)}'` : `src="${src}"`} />`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(/<div([^>]*)id="giric"([^>]*)>/, (match, group1, group2) => {
|
||||
if (group1.indexOf(' style="') === -1 && group2.indexOf(' style="') === -1) {
|
||||
return `<div${group1}id="giric"${group2} style="display: block;">`
|
||||
}
|
||||
return match
|
||||
})
|
||||
.replace(/(?<=<div[^>]*?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
|
||||
}
|
||||
}
|
||||
@@ -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 += `<br>${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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user