Skip to content

Commit 386d827

Browse files
timefoxrobintimefox
authored
feat(bing): add the image generation function (#381)
* import '@timefox/bic-sydney' module to enbale BingAIClient's image generation capability. * adding options for setting 'x-forwarded-for' and whether to enable image generation for bingAiClient. * make a slight optimization for bingAiClient's image generation function. * fix eslint error Trailing spaces not allowed no-trailing-spaces. * Fixed several issues pointed out by waylaidwanderer . * Remove duplicate key 'cookie'. * Fix eslint error Trailing spaces not allowed no-trailing-spaces. * Use the try-catch-await pattern for waiting for the image generation and fixed the issue of option 'xForwardedFor'. * Do a mirror optimization. * Fixed the crash when bic throw an error. --------- Co-authored-by: robin <[email protected]> Co-authored-by: timefox <[email protected]>
1 parent d407bf1 commit 386d827

File tree

5 files changed

+110
-7
lines changed

5 files changed

+110
-7
lines changed

bin/cli.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ if (settings.storageFilePath && !settings.cacheOptions.store) {
4040
settings.cacheOptions.store = new KeyvFile({ filename: settings.storageFilePath });
4141
}
4242

43+
// Disable the image generation in cli mode always.
44+
settings.bingAiClient.features = settings.bingAiClient.features || {};
45+
settings.bingAiClient.features.genImage = false;
46+
4347
let conversationData = {};
4448

4549
const availableCommands = [

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"dependencies": {
4343
"@dqbd/tiktoken": "^1.0.2",
4444
"@fastify/cors": "^8.2.0",
45+
"@timefox/bic-sydney": "^1.1.2",
4546
"@waylaidwanderer/fastify-sse-v2": "^3.1.0",
4647
"@waylaidwanderer/fetch-event-source": "^3.0.1",
4748
"boxen": "^7.0.1",

settings.example.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ export default {
4646
cookies: '',
4747
// A proxy string like "http://<ip>:<port>"
4848
proxy: '',
49+
// (Optional) Set 'x-forwarded-for' for the request. You can use a fixed IPv4 address or specify a range using CIDR notation,
50+
// and the program will randomly select an address within that range. The 'x-forwarded-for' is not used by default now.
51+
// xForwardedFor: '13.104.0.0/14',
52+
// (Optional) Set 'genImage' to true to enable bing to create images for you. It's disabled by default.
53+
// features: {
54+
// genImage: true,
55+
// },
4956
// (Optional) Set to true to enable `console.debug()` logging
5057
debug: false,
5158
},

src/BingAIClient.js

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import WebSocket from 'ws';
44
import Keyv from 'keyv';
55
import { ProxyAgent } from 'undici';
66
import { HttpsProxyAgent } from 'https-proxy-agent';
7+
import { BingImageCreator } from '@timefox/bic-sydney';
78

89
/**
910
* https://stackoverflow.com/a/58326357
@@ -39,21 +40,53 @@ export default class BingAIClient {
3940
this.options = {
4041
...options,
4142
host: options.host || 'https://www.bing.com',
43+
xForwardedFor: this.constructor.getValidIPv4(options.xForwardedFor),
44+
features: {
45+
genImage: options?.features?.genImage || false,
46+
},
4247
};
4348
}
4449
this.debug = this.options.debug;
50+
if (this.options.features.genImage) {
51+
this.bic = new BingImageCreator(this.options);
52+
}
53+
}
54+
55+
static getValidIPv4(ip) {
56+
const match = !ip
57+
|| 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]))?$/);
58+
if (match) {
59+
if (match[5]) {
60+
const mask = parseInt(match[5], 10);
61+
let [a, b, c, d] = ip.split('.').map(x => parseInt(x, 10));
62+
// eslint-disable-next-line no-bitwise
63+
const max = (1 << (32 - mask)) - 1;
64+
const rand = Math.floor(Math.random() * max);
65+
d += rand;
66+
c += Math.floor(d / 256);
67+
d %= 256;
68+
b += Math.floor(c / 256);
69+
c %= 256;
70+
a += Math.floor(b / 256);
71+
b %= 256;
72+
return `${a}.${b}.${c}.${d}`;
73+
}
74+
return ip;
75+
}
76+
return undefined;
4577
}
4678

4779
async createNewConversation() {
4880
const fetchOptions = {
4981
headers: {
5082
accept: 'application/json',
5183
'accept-language': 'en-US,en;q=0.9',
52-
'sec-ch-ua': '"Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"',
84+
'content-type': 'application/json',
85+
'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"',
5386
'sec-ch-ua-arch': '"x86"',
5487
'sec-ch-ua-bitness': '"64"',
55-
'sec-ch-ua-full-version': '"115.0.1866.1"',
56-
'sec-ch-ua-full-version-list': '"Not/A)Brand";v="99.0.0.0", "Microsoft Edge";v="115.0.1866.1", "Chromium";v="115.0.5767.0"',
88+
'sec-ch-ua-full-version': '"113.0.1774.50"',
89+
'sec-ch-ua-full-version-list': '"Microsoft Edge";v="113.0.1774.50", "Chromium";v="113.0.5672.127", "Not-A.Brand";v="24.0.0.0"',
5790
'sec-ch-ua-mobile': '?0',
5891
'sec-ch-ua-model': '""',
5992
'sec-ch-ua-platform': '"Windows"',
@@ -65,11 +98,13 @@ export default class BingAIClient {
6598
'sec-ms-gec-version': '1-115.0.1866.1',
6699
'x-ms-client-request-id': crypto.randomUUID(),
67100
'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32',
101+
'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',
102+
cookie: this.options.cookies || (this.options.userToken ? `_U=${this.options.userToken}` : undefined),
68103
Referer: 'https://www.bing.com/search?q=Bing+AI&showconv=1',
69104
'Referrer-Policy': 'origin-when-cross-origin',
70-
cookie: this.options.cookies || (this.options.userToken ? `_U=${this.options.userToken}` : undefined),
71105
// Workaround for request being blocked due to geolocation
72-
'x-forwarded-for': '1.1.1.1',
106+
// 'x-forwarded-for': '1.1.1.1', // 1.1.1.1 seems to no longer work.
107+
...(this.options.xForwardedFor ? { 'x-forwarded-for': this.options.xForwardedFor } : {}),
73108
},
74109
};
75110
if (this.options.proxy) {
@@ -310,6 +345,7 @@ export default class BingAIClient {
310345
'cricinfov2',
311346
'dv3sugg',
312347
'nojbfedge',
348+
...((toneStyle === 'creative' && this.options.features.genImage) ? ['gencontentv3'] : []),
313349
],
314350
sliceIds: [
315351
'222dtappid',
@@ -378,7 +414,8 @@ export default class BingAIClient {
378414
reject(new Error('Request aborted'));
379415
});
380416

381-
ws.on('message', (data) => {
417+
let bicIframe;
418+
ws.on('message', async (data) => {
382419
const objects = data.toString().split('');
383420
const events = objects.map((object) => {
384421
try {
@@ -400,6 +437,19 @@ export default class BingAIClient {
400437
if (!messages?.length || messages[0].author !== 'bot') {
401438
return;
402439
}
440+
if (messages[0]?.contentType === 'IMAGE') {
441+
// You will never get a message of this type without 'gencontentv3' being on.
442+
bicIframe = this.bic.genImageIframeSsr(
443+
messages[0].text,
444+
messages[0].messageId,
445+
progress => (progress?.contentIframe ? onProgress(progress?.contentIframe) : null),
446+
).catch((error) => {
447+
onProgress(error.message);
448+
bicIframe.isError = true;
449+
return error.message;
450+
});
451+
return;
452+
}
403453
const updatedText = messages[0].text;
404454
if (!updatedText || updatedText === replySoFar) {
405455
return;
@@ -424,7 +474,7 @@ export default class BingAIClient {
424474
return;
425475
}
426476
const messages = event.item?.messages || [];
427-
const eventMessage = messages.length ? messages[messages.length - 1] : null;
477+
let eventMessage = messages.length ? messages[messages.length - 1] : null;
428478
if (event.item?.result?.error) {
429479
if (this.debug) {
430480
console.debug(event.item.result.value, event.item.result.message);
@@ -469,6 +519,23 @@ export default class BingAIClient {
469519
// delete useless suggestions from moderation filter
470520
delete eventMessage.suggestedResponses;
471521
}
522+
if (bicIframe) {
523+
// the last messages will be a image creation event if bicIframe is present.
524+
let i = messages.length - 1;
525+
while (eventMessage?.contentType === 'IMAGE' && i > 0) {
526+
eventMessage = messages[i -= 1];
527+
}
528+
529+
// wait for bicIframe to be completed.
530+
// since we added a catch, we do not need to wrap this with a try catch block.
531+
const imgIframe = await bicIframe;
532+
if (!imgIframe?.isError) {
533+
eventMessage.adaptiveCards[0].body[0].text += imgIframe;
534+
} else {
535+
eventMessage.text += `<br>${imgIframe}`;
536+
eventMessage.adaptiveCards[0].body[0].text = eventMessage.text;
537+
}
538+
}
472539
resolve({
473540
message: eventMessage,
474541
conversationExpiryTime: event?.item?.conversationExpiryTime,
@@ -485,6 +552,11 @@ export default class BingAIClient {
485552
return;
486553
}
487554
default:
555+
if (event?.error) {
556+
clearTimeout(messageTimeout);
557+
this.constructor.cleanupWebSocketConnection(ws);
558+
reject(new Error(`Event Type('${event.type}'): ${event.error}`));
559+
}
488560
// eslint-disable-next-line no-useless-return
489561
return;
490562
}

0 commit comments

Comments
 (0)