diff --git a/package-lock.json b/package-lock.json index ac4396d6..140e6819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,6 +90,7 @@ "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "cross-env": "^7.0.3", + "openai": "^4.7.1", "sass": "^1.66.1" } }, @@ -3991,6 +3992,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.5.tgz", + "integrity": "sha512-OZsUlr2nxvkqUFLSaY2ZbA+P1q22q+KrlxWOn/38RX+u5kTkYL2mTujEpzUhGkS+K/QCYp9oagfXG39XOzyySg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -4300,6 +4311,18 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -4330,6 +4353,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dev": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4632,6 +4667,12 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", + "dev": true + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -5331,6 +5372,16 @@ "node": ">=0.3.1" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dev": true, + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -6000,6 +6051,15 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/execa": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", @@ -6163,6 +6223,12 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "dev": true + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -6171,6 +6237,19 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dev": true, + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -7288,6 +7367,15 @@ "node": ">=14.18.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/i18next": { "version": "23.4.9", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.4.9.tgz", @@ -9236,6 +9324,45 @@ "tslib": "^2.0.3" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -9506,6 +9633,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.7.1.tgz", + "integrity": "sha512-4Um9A4aLGzZxyENyway0zVgi69BOxaqXmjOCKp3PUteOvSn9TeVf6IjkaNY8k/LXYG5l2e7PpacOl2sxsrTc/w==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.17.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.17.tgz", + "integrity": "sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==", + "dev": true + }, "node_modules/openid-client": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.3.tgz", @@ -11381,6 +11533,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -11996,6 +12154,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, "node_modules/websocket-as-promised": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/websocket-as-promised/-/websocket-as-promised-2.0.1.tgz", @@ -12010,6 +12183,16 @@ "node": ">=6" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 98a2e5c7..d9972a17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bingo", - "version": "0.7.0", + "version": "0.8.0", "private": true, "main": "./cloudflare/cli.js", "scripts": { @@ -92,6 +92,7 @@ "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "cross-env": "^7.0.3", + "openai": "^4.7.1", "sass": "^1.66.1" } } diff --git a/src/lib/bots/bing/index.ts b/src/lib/bots/bing/index.ts index f162c55d..23cf1d30 100644 --- a/src/lib/bots/bing/index.ts +++ b/src/lib/bots/bing/index.ts @@ -119,22 +119,18 @@ const getOptionSets = (conversationStyle: BingConversationStyle) => { export class BingWebBot { protected conversationContext?: ConversationInfo - protected cookie: string - protected ua: string protected endpoint = '' + protected cookie = '' private lastText = '' private asyncTasks: Array> = [] constructor(opts: { - cookie: string - ua: string - bingConversationStyle?: BingConversationStyle - conversationContext?: ConversationInfo + endpoint?: string + cookie?: string }) { - const { cookie, ua, conversationContext } = opts - this.cookie = cookie?.includes(';') ? cookie : `_EDGE_V=1; _U=${cookie}` - this.ua = ua - this.conversationContext = conversationContext + const { endpoint, cookie } = opts + this.endpoint = endpoint || '' + this.cookie = cookie || '' } static buildChatRequest(conversation: ConversationInfo) { @@ -235,7 +231,6 @@ export class BingWebBot { async createConversation(conversationId?: string): Promise { const headers = { 'Accept-Encoding': 'gzip, deflate, br, zsdch', - 'User-Agent': this.ua, 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32', cookie: this.cookie, } @@ -276,7 +271,7 @@ export class BingWebBot { return resp } - private async createContext(conversationStyle: BingConversationStyle, conversation?: ConversationInfoBase) { + async createContext(conversationStyle: BingConversationStyle, conversation?: ConversationInfoBase) { if (!this.conversationContext) { conversation = conversation?.conversationSignature ? conversation : await this.createConversation() as unknown as ConversationInfo this.conversationContext = { @@ -312,6 +307,7 @@ export class BingWebBot { method: 'POST', headers: { 'Content-Type': 'application/json', + cookie: this.cookie, }, signal: abortController.signal, body: JSON.stringify(this.conversationContext!) @@ -344,7 +340,6 @@ export class BingWebBot { headers: { 'accept-language': 'zh-CN,zh;q=0.9', 'cache-control': 'no-cache', - 'User-Agent': this.ua, pragma: 'no-cache', cookie: this.cookie, } @@ -364,10 +359,10 @@ export class BingWebBot { private async createImage(prompt: string, id: string) { const headers = { 'Accept-Encoding': 'gzip, deflate, br, zsdch', - 'User-Agent': this.ua, 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32', cookie: this.cookie, } + const query = new URLSearchParams({ prompt, id @@ -456,7 +451,7 @@ export class BingWebBot { private async parseEvents(params: Params, events: any) { events?.forEach(async (event: ChatUpdateCompleteResponse) => { - debug('bing event', event) + debug('bing event', JSON.stringify(event)) if (event.type === 3) { await Promise.all(this.asyncTasks) .catch(error => { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0d9a47b2..15e14bb8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -141,9 +141,9 @@ export function parseUA(ua?: string, default_ua = DEFAULT_UA) { export function mockUser(cookies: Partial<{ [key: string]: string }>) { const { - BING_HEADER, + BING_HEADER = process.env.BING_HEADER || '', BING_UA = process.env.BING_UA, - BING_IP = '', + BING_IP = process.env.BING_IP || '', } = cookies const ua = parseUA(BING_UA) @@ -165,7 +165,7 @@ export function mockUser(cookies: Partial<{ [key: string]: string }>) { export function createHeaders(cookies: Partial<{ [key: string]: string }>, type?: 'image') { let { BING_HEADER = process.env.BING_HEADER, - BING_IP = '', + BING_IP = process.env.BING_IP || '', IMAGE_ONLY = process.env.IMAGE_ONLY ?? '1', } = cookies || {} const imageOnly = /^(1|true|yes)$/.test(String(IMAGE_ONLY)) diff --git a/src/pages/api/chat/completions.ts b/src/pages/api/chat/completions.ts new file mode 100644 index 00000000..6335a123 --- /dev/null +++ b/src/pages/api/chat/completions.ts @@ -0,0 +1,104 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import assert from 'assert' +import { BingWebBot } from '@/lib/bots/bing' +import { BingConversationStyle, ConversationInfoBase } from '@/lib/bots/bing/types' + +export const config = { + api: { + responseLimit: false, + }, +} + +export type Role = 'user' | 'assistant' +export type Action = 'next' | 'variant'; + +export interface APIMessage { + role: Role + content: string +} + +export interface APIRequest { + id?: string + model: string + action: Action + messages: APIMessage[] + stream?: boolean +} + +export interface APIResponse { + id?: string + choices: { + delta?: APIMessage + message: APIMessage + }[] +} + +function parseOpenAIMessage(request: APIRequest) { + return { + prompt: request.messages?.reverse().find((message) => message.role === 'user')?.content, + stream: request.stream, + }; +} + +function responseOpenAIMessage(content: string, id?: string): APIResponse { + const message: APIMessage = { + role: 'assistant', + content, + }; + return { + id, + choices: [{ + delta: message, + message, + }], + }; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { prompt, stream } = parseOpenAIMessage(req.body); + let { id } = req.body + const chatbot = new BingWebBot({ + endpoint: 'http://127.0.0.1:3000' || req.headers.origin, + cookie: `BING_IP=${process.env.BING_IP}` + }) + id ||= JSON.stringify(chatbot.createConversation()) + + if (stream) { + res.setHeader('Content-Type', 'text/event-stream; charset=utf-8') + } + let lastLength = 0 + let lastText = '' + const abortController = new AbortController() + assert(prompt, 'messages can\'t be empty!') + + chatbot.sendMessage({ + prompt, + options: { + bingConversationStyle: BingConversationStyle.Creative, + conversation: JSON.parse(id) as ConversationInfoBase + }, + signal: abortController.signal, + onEvent(event) { + if (event.type === 'UPDATE_ANSWER') { + lastText = event.data.text + if (stream && lastLength !== lastText.length) { + res.write(`data: ${JSON.stringify(responseOpenAIMessage(lastText.slice(lastLength), id))}\n`) + res.flushHeaders() + lastLength = lastText.length + } + } else if (event.type === 'ERROR') { + res.write(`data: ${JSON.stringify(responseOpenAIMessage(`${event.error}`, id))}\n`) + res.flushHeaders() + } else if (event.type === 'DONE') { + if (stream) { + res.end(`data: [DONE]\n`); + } else { + res.json(responseOpenAIMessage(lastText, id)) + } + } + }, + }) + req.socket.once('close', () => { + abortController.abort() + }) +} diff --git a/src/pages/api/create.ts b/src/pages/api/create.ts index f53e9038..a8d4646a 100644 --- a/src/pages/api/create.ts +++ b/src/pages/api/create.ts @@ -2,7 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next' import { fetch, debug } from '@/lib/isomorphic' -import { createHeaders, randomIP, extraHeadersFromCookie } from '@/lib/utils' +import { createHeaders, randomIP } from '@/lib/utils' import { sleep } from '@/lib/bots/bing/utils' export default async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/src/state/index.ts b/src/state/index.ts index 65726831..ea78257d 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -37,10 +37,7 @@ export const historyAtom = atomWithStorage('enableHistory', false, unde export const localPromptsAtom = atomWithStorage('prompts', [], undefined, { unstable_getOnInit: true }) const createBotInstance = () => { - return new BingWebBot({ - cookie: ' ', - ua: ' ', - }) + return new BingWebBot({}) } export const chatHistoryAtom = atomWithStorage<{ diff --git a/tests/openai-stream.ts b/tests/openai-stream.ts new file mode 100644 index 00000000..0360e1de --- /dev/null +++ b/tests/openai-stream.ts @@ -0,0 +1,21 @@ +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: 'dummy', + baseURL: 'http://127.0.0.1:3000/api' +}); + +async function start() { + const completion = await openai.chat.completions.create({ + messages: [ + { role: 'user', content: '你好' }, + ], + model: 'bing', + stream: true, + }); + for await (const part of completion) { + process.stdout.write(part.choices[0]?.delta?.content || ''); + } +} + +start() diff --git a/tests/openai.ts b/tests/openai.ts new file mode 100644 index 00000000..3159de20 --- /dev/null +++ b/tests/openai.ts @@ -0,0 +1,18 @@ +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: 'dummy', + baseURL: 'http://127.0.0.1:3000/api' +}); + +async function start() { + const completion = await openai.chat.completions.create({ + messages: [ + { role: 'user', content: '你好' }, + ], + model: 'bing', + }); + console.log(completion.choices[0].message.content) +} + +start()