@itzraiss/bic-sydney
Advanced tools
Comparing version 1.1.8 to 1.1.9
{ | ||
"name": "@itzraiss/bic-sydney", | ||
"version": "1.1.8", | ||
"version": "1.1.9", | ||
"description": "This is a module which allows sydney to use the Copilot Image Creator to generate images for you.", | ||
@@ -35,3 +35,3 @@ "main": "index.js", | ||
"fetch-undici": "^4.0.0", | ||
"undici": "^6.6.0" | ||
"undici": "^6.6.1" | ||
}, | ||
@@ -38,0 +38,0 @@ "devDependencies": { |
@@ -1,517 +0,498 @@ | ||
/** | ||
* A module that provides some 'Copilot Image Creator' functions. | ||
* @module CopilotImageCreator | ||
*/ | ||
import { ProxyAgent } from 'undici'; | ||
import { fetch as fetchUndici } from 'fetch-undici'; | ||
let fetch; | ||
/** | ||
* @class | ||
* @description A class that creates the images using the 'Copilot Image Creator'. | ||
* A class that creates the images using the 'Copilot Image Creator'. | ||
* @module CopilotImageCreator | ||
*/ | ||
export default class CopilotImageCreator { | ||
/** | ||
* @constructor | ||
* @param {Object} options - Options for CopilotImageCreator. | ||
*/ | ||
constructor(options) { | ||
this.setOptions(options); | ||
/** | ||
* @constructor | ||
* @param {Object} options - Options for CopilotImageCreator. | ||
*/ | ||
constructor(options) { | ||
this.setOptions(options); | ||
} | ||
/** | ||
* Set options for CopilotImageCreator. | ||
* @param {Object} options - Options for CopilotImageCreator. The format of the options is almost same as the CopilotAiClient 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/121.0.0.0 Safari/537.36 Edg/121.0.0.0', | ||
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, | ||
}, | ||
}; | ||
} | ||
fetch = typeof options.fetch === 'function' ? options.fetch : fetchUndici; | ||
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; | ||
} | ||
/** | ||
* Set options for CopilotImageCreator. | ||
* @param {Object} options - Options for CopilotImageCreator. The format of the options is almost same as the CopilotAiClient 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/121.0.0.0 Safari/537.36 Edg/121.0.0.0', | ||
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, | ||
}, | ||
}; | ||
} | ||
fetch = typeof options.fetch === 'function' ? options.fetch : fetchUndici; | ||
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}` : '') : ''; | ||
}, | ||
/** | ||
* Get fetchOptions of CopilotImageCreator. | ||
* {Object} The fetch options used for CopilotImageCreator. | ||
*/ | ||
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,,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', | ||
'cache-control': 'no-cache', | ||
'sec-ch-ua': '"Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"', | ||
'sec-ch-ua-arch': '"x86"', | ||
'sec-ch-ua-bitness': '"64"', | ||
'sec-ch-ua-full-version': '"121.0.2277.98"', | ||
'sec-ch-ua-full-version-list': '"Not A(Brand";v="99.0.0.0", "Microsoft Edge";v="121.0.2277.98", "Chromium";v="121.0.6167.139"', | ||
'sec-ch-ua-mobile': '?0', | ||
'sec-ch-ua-model': '""', | ||
'sec-ch-ua-platform': '"Windows"', | ||
'sec-ch-ua-platform-version': '"15.0.0"', | ||
'sec-fetch-dest': 'iframe', | ||
'sec-fetch-mode': 'no-cors', | ||
'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', | ||
}, | ||
}; | ||
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; | ||
if (this.options.proxy) { | ||
fetchOptions.dispatcher = new ProxyAgent(this.options.proxy); | ||
} | ||
return undefined; | ||
} | ||
} | ||
return fetchOptions; | ||
})(); | ||
} | ||
/** | ||
* Get fetchOptions of CopilotImageCreator. | ||
* {Object} The fetch options used for CopilotImageCreator. | ||
*/ | ||
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,,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', | ||
'cache-control': 'no-cache', | ||
'sec-ch-ua': '"Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"', | ||
'sec-ch-ua-arch': '"x86"', | ||
'sec-ch-ua-bitness': '"64"', | ||
'sec-ch-ua-full-version': '"121.0.2277.98"', | ||
'sec-ch-ua-full-version-list': '"Not A(Brand";v="99.0.0.0", "Microsoft Edge";v="121.0.2277.98", "Chromium";v="121.0.6167.139"', | ||
'sec-ch-ua-mobile': '?0', | ||
'sec-ch-ua-model': '""', | ||
'sec-ch-ua-platform': '"Windows"', | ||
'sec-ch-ua-platform-version': '"15.0.0"', | ||
'sec-fetch-dest': 'iframe', | ||
'sec-fetch-mode': 'no-cors', | ||
'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', | ||
}, | ||
}; | ||
/** | ||
* 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}"[^>]*>`); | ||
if (this.options.proxy) { | ||
fetchOptions.dispatcher = new ProxyAgent(this.options.proxy); | ||
} | ||
} | ||
// Find out the start and end position of <tag id="tagId">. | ||
const match = regex.exec(html); | ||
return fetchOptions; | ||
})(); | ||
// return the original html if nothing matches. | ||
if (!match) { | ||
return html; | ||
} | ||
const start = match.index; | ||
let end = match.index + match[0].length; | ||
/** | ||
* 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); | ||
} | ||
// 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}>`; | ||
/** | ||
* 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; | ||
// 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); | ||
} | ||
const start = match.index; | ||
let end = match.index + match[0].length; | ||
/** | ||
* 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. | ||
*/ | ||
// 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}>`; | ||
/** | ||
* 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()}`; | ||
} | ||
// 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; | ||
} | ||
} | ||
} | ||
// 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}` : ''}`; | ||
// Remove the strings between the '<tag id="tagId">' and the matched '</tag>'. | ||
return html.slice(0, start) + html.slice(end); | ||
if (this.debug) { | ||
console.debug(`The url of the request for image creation: ${url}`); | ||
console.debug(); | ||
} | ||
/** | ||
* 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)); | ||
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(); | ||
} | ||
/** | ||
* @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. | ||
*/ | ||
if (status !== 200) { | ||
throw new Error(`Copilot Image Creator Error: response status = ${status}`); | ||
} | ||
/** | ||
* 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()}`; | ||
} | ||
const body = await response.text(); | ||
let regex = /<div id="gir" data-c="([^"]*)"/; | ||
const pollingUrl = regex.exec(body)?.[1]; | ||
// 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 (!pollingUrl) { | ||
regex = /<div class="gil_err_mt">(.*?)<\/div>/; | ||
const err = regex.exec(body)?.[1]; | ||
throw new Error(`Copilot Image Creator Error: ${err}`); | ||
} | ||
if (this.debug) { | ||
console.debug(`The url of the request for image creation: ${url}`); | ||
console.debug(); | ||
} | ||
return { | ||
contentUrl: `${response.url}`, | ||
pollingUrl: `${this.options.host}${this.constructor.decodeHtmlLite(pollingUrl)}`, | ||
contentHtml: body, | ||
prompt: `${prompt}`, | ||
iframeid: `${messageId}`, | ||
}; | ||
} | ||
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(); | ||
} | ||
/** | ||
* @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. | ||
*/ | ||
if (status !== 200) { | ||
throw new Error(`Copilot Image Creator Error: response status = ${status}`); | ||
} | ||
/** | ||
* 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; | ||
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(`Copilot Image Creator Error: ${err}`); | ||
} | ||
return { | ||
contentUrl: `${response.url}`, | ||
pollingUrl: `${this.options.host}${this.constructor.decodeHtmlLite(pollingUrl)}`, | ||
contentHtml: body, | ||
prompt: `${prompt}`, | ||
iframeid: `${messageId}`, | ||
}; | ||
if (typeof onProgress !== 'function') { | ||
onProgress = () => false; | ||
} | ||
/** | ||
* @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. | ||
*/ | ||
const pollingStartTime = new Date().getTime(); | ||
/** | ||
* 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; | ||
while (polling) { | ||
if (this.debug) { | ||
console.debug(`polling the image request: ${pollingUrl}`); | ||
} | ||
if (typeof onProgress !== 'function') { | ||
onProgress = () => false; | ||
} | ||
// eslint-disable-next-line no-await-in-loop | ||
const response = await fetch(pollingUrl, this.fetchOptions); | ||
const { status } = response; | ||
const pollingStartTime = new Date().getTime(); | ||
if (status !== 200) { | ||
throw new Error(`Copilot Image Creator Error: response status = ${status}`); | ||
} | ||
while (polling) { | ||
if (this.debug) { | ||
console.debug(`polling the image request: ${pollingUrl}`); | ||
} | ||
// eslint-disable-next-line no-await-in-loop | ||
body = await response.text(); | ||
// eslint-disable-next-line no-await-in-loop | ||
const response = await fetch(pollingUrl, this.fetchOptions); | ||
const { status } = response; | ||
if (status !== 200) { | ||
throw new Error(`Copilot 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('Copilot Image Creator Error: cancelled'); | ||
} | ||
// eslint-disable-next-line no-await-in-loop | ||
await this.constructor.sleep(1000); | ||
} | ||
if (body && body.indexOf('errorMessage') === -1) { | ||
polling = false; | ||
} else { | ||
const cancelRequest = onProgress({ pollingStartTime }); | ||
if (cancelRequest) { | ||
throw new Error('Copilot Image Creator Error: cancelled'); | ||
} | ||
return body; | ||
// eslint-disable-next-line no-await-in-loop | ||
await this.constructor.sleep(1000); | ||
} | ||
} | ||
/** | ||
* 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(); | ||
} | ||
return body; | ||
} | ||
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; | ||
})(), | ||
); | ||
/** | ||
* 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(); | ||
} | ||
/** | ||
* 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}"`} />`; | ||
} | ||
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; | ||
})(), | ||
); | ||
} | ||
/** | ||
* 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}/`); | ||
} | ||
/** | ||
* 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);" ' | ||
+ `${isDoc ? `srcdoc='${this.rewriteHtml(src)}'` : `src="${src}"`} />`; | ||
} | ||
/** | ||
* 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 CopilotAI. | ||
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); | ||
} | ||
/** | ||
* 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}/`); | ||
} | ||
/** | ||
* 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 Copilot 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('Copilot Image Creator Error: cancelled'); | ||
} | ||
/** | ||
* 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 CopilotAI. | ||
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;">`; | ||
} | ||
const resultHtml = await this.pollingImgRequest(pollingUrl, onProgress); | ||
return this.renderImageIframe(contentHtml, resultHtml); | ||
} | ||
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 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 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 Copilot 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('Copilot Image Creator Error: cancelled'); | ||
} | ||
} | ||
const resultHtml = await this.pollingImgRequest(pollingUrl, onProgress); | ||
return this.renderImageIframe(contentHtml, resultHtml); | ||
} | ||
/** | ||
* 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 Copilot.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); | ||
} | ||
/** | ||
* 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); | ||
} | ||
/** | ||
* The pattern to match the inline image generation request. | ||
*/ | ||
static get inlineImagePattern() { | ||
return /!\[(.*?)\]\(#generative_image\)/g; | ||
} | ||
/** | ||
* 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 Copilot.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); | ||
} | ||
/** | ||
* Why is there such a function here? I have seen the messages with inline generative image style at a converation with Copilot, but only once. | ||
* The message contains a markdown tag like '![prompt](#generative_image)', 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; | ||
} | ||
/** | ||
* The pattern to match the inline image generation request. | ||
*/ | ||
static get inlineImagePattern() { | ||
return /!\[(.*?)\]\(#generative_image\)/g; | ||
} | ||
const match = CopilotImageCreator.inlineImagePattern.exec(message); | ||
if (match) { | ||
return match[1]; | ||
} | ||
/** | ||
* Why is there such a function here? I have seen the messages with inline generative image style at a converation with Copilot, but only once. | ||
* The message contains a markdown tag like '![prompt](#generative_image)', 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; | ||
} | ||
return undefined; | ||
const match = CopilotImageCreator.inlineImagePattern.exec(message); | ||
if (match) { | ||
return match[1]; | ||
} | ||
return undefined; | ||
} | ||
} |
26372
4
453
Updatedundici@^6.6.1