From e71ee74c6d30d4509e06f9523ed871a70acf6ef2 Mon Sep 17 00:00:00 2001 From: josc146 Date: Wed, 6 Sep 2023 23:52:28 +0800 Subject: [PATCH] patch: fix(upgrade) bing client (#505) --- build.mjs | 1 + src/background/index.mjs | 42 ++++- src/components/ConversationCard/index.jsx | 183 ++++++++++++++-------- src/config/index.mjs | 1 + src/manifest.json | 24 ++- src/manifest.v2.json | 6 +- src/rules.json | 24 +++ src/services/clients/bing/index.mjs | 98 ++++++------ src/utils/fetch-bg.mjs | 30 ++++ 9 files changed, 287 insertions(+), 122 deletions(-) create mode 100644 src/rules.json create mode 100644 src/utils/fetch-bg.mjs diff --git a/build.mjs b/build.mjs index 2d2a3c7b..0baf44fb 100644 --- a/build.mjs +++ b/build.mjs @@ -249,6 +249,7 @@ async function copyFiles(entryPoints, targetDir) { async function finishOutput(outputDirSuffix) { const commonFiles = [ { src: 'src/logo.png', dst: 'logo.png' }, + { src: 'src/rules.json', dst: 'rules.json' }, { src: 'build/shared.js', dst: 'shared.js' }, { src: 'build/content-script.css', dst: 'content-script.css' }, // shared diff --git a/src/background/index.mjs b/src/background/index.mjs index 58797141..e17820f6 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -83,7 +83,7 @@ async function executeApi(session, port, config) { const accessToken = await getChatGptAccessToken() await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken) } - } else if (bingWebModelKeys.some((n) => session.modelName.includes(n))) { + } else if (bingWebModelKeys.includes(session.modelName)) { const accessToken = await getBingAccessToken() if (session.modelName.includes('bingFreeSydney')) await generateAnswersWithBingWebApi(port, session.question, session, accessToken, true) @@ -192,9 +192,49 @@ Browser.runtime.onMessage.addListener(async (message, sender) => { } break } + case 'FETCH': { + if (message.data.input.includes('bing.com')) { + const accessToken = await getBingAccessToken() + await setUserConfig({ bingAccessToken: accessToken }) + } + + try { + const response = await fetch(message.data.input, message.data.init) + const text = await response.text() + return [ + { + body: text, + status: response.status, + statusText: response.statusText, + }, + null, + ] + } catch (error) { + return [null, error] + } + } } }) +Browser.webRequest.onBeforeSendHeaders.addListener( + (details) => { + const headers = details.requestHeaders + for (let i = 0; i < headers.length; i++) { + if (headers[i].name === 'Origin') { + headers[i].value = 'https://www.bing.com' + } else if (headers[i].name === 'Referer') { + headers[i].value = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' + } + } + return { requestHeaders: headers } + }, + { + urls: ['wss://sydney.bing.com/*', 'https://www.bing.com/*'], + types: ['xmlhttprequest', 'websocket'], + }, + ['requestHeaders'], +) + registerPortListener(async (session, port, config) => await executeApi(session, port, config)) registerCommands() refreshMenu() diff --git a/src/components/ConversationCard/index.jsx b/src/components/ConversationCard/index.jsx index d0241fe4..16d3d418 100644 --- a/src/components/ConversationCard/index.jsx +++ b/src/components/ConversationCard/index.jsx @@ -5,17 +5,17 @@ import InputBox from '../InputBox' import ConversationItem from '../ConversationItem' import { createElementAtPosition, isFirefox, isMobile, isSafari } from '../../utils' import { - LinkExternalIcon, ArchiveIcon, DesktopDownloadIcon, + LinkExternalIcon, MoveToBottomIcon, } from '@primer/octicons-react' -import { WindowDesktop, XLg, Pin } from 'react-bootstrap-icons' +import { Pin, WindowDesktop, XLg } from 'react-bootstrap-icons' import FileSaver from 'file-saver' import { render } from 'preact' import FloatingToolbar from '../FloatingToolbar' import { useClampWindowSize } from '../../hooks/use-clamp-window-size' -import { ModelMode, Models } from '../../config/index.mjs' +import { bingWebModelKeys, getUserConfig, ModelMode, Models } from '../../config/index.mjs' import { useTranslation } from 'react-i18next' import DeleteButton from '../DeleteButton' import { useConfig } from '../../hooks/use-config.mjs' @@ -23,6 +23,7 @@ import { createSession } from '../../services/local-session.mjs' import { v4 as uuidv4 } from 'uuid' import { initSession } from '../../services/init-session.mjs' import { findLastIndex } from 'lodash-es' +import { generateAnswersWithBingWebApi } from '../../services/apis/bing-web.mjs' const logo = Browser.runtime.getURL('logo.png') @@ -48,6 +49,7 @@ function ConversationCard(props) { const windowSize = useClampWindowSize([750, 1500], [250, 1100]) const bodyRef = useRef(null) const [completeDraggable, setCompleteDraggable] = useState(false) + const useForegroundFetch = bingWebModelKeys.includes(session.modelName) /** * @type {[ConversationItemData[], (conversationItemData: ConversationItemData[]) => void]} @@ -96,12 +98,12 @@ function ConversationCard(props) { } }, [conversationItemData]) - useEffect(() => { + useEffect(async () => { // when the page is responsive, session may accumulate redundant data and needs to be cleared after remounting and before making a new request if (props.question) { const newSession = initSession({ question: props.question }) setSession(newSession) - port.postMessage({ session: newSession }) + await postMessage({ session: newSession }) } }, [props.question]) // usually only triggered once @@ -125,6 +127,100 @@ function ConversationCard(props) { }) } + const portMessageListener = (msg) => { + if (msg.answer) { + updateAnswer(msg.answer, false, 'answer') + } + if (msg.session) { + if (msg.done) msg.session = { ...msg.session, isRetry: false } + setSession(msg.session) + } + if (msg.done) { + updateAnswer('', true, 'answer', true) + setIsReady(true) + } + if (msg.error) { + switch (msg.error) { + case 'UNAUTHORIZED': + updateAnswer( + `${t('UNAUTHORIZED')}
${t('Please login at https://chat.openai.com first')}${ + isSafari() ? `
${t('Then open https://chat.openai.com/api/auth/session')}` : '' + }
${t('And refresh this page or type you question again')}` + + `

${t( + 'Consider creating an api key at https://platform.openai.com/account/api-keys', + )}`, + false, + 'error', + ) + break + case 'CLOUDFLARE': + updateAnswer( + `${t('OpenAI Security Check Required')}
${ + isSafari() + ? t('Please open https://chat.openai.com/api/auth/session') + : t('Please open https://chat.openai.com') + }
${t('And refresh this page or type you question again')}` + + `

${t( + 'Consider creating an api key at https://platform.openai.com/account/api-keys', + )}`, + false, + 'error', + ) + break + default: + if (conversationItemData[conversationItemData.length - 1].content.includes('gpt-loading')) + updateAnswer(msg.error, false, 'error') + else + setConversationItemData([ + ...conversationItemData, + new ConversationItemData('error', msg.error), + ]) + break + } + setIsReady(true) + } + } + + const foregroundMessageListeners = useRef([]) + + /** + * @param {Session|undefined} session + * @param {boolean|undefined} stop + */ + const postMessage = async ({ session, stop }) => { + if (useForegroundFetch) { + foregroundMessageListeners.current.forEach((listener) => listener({ session, stop })) + if (session) { + const fakePort = { + postMessage: (msg) => { + portMessageListener(msg) + }, + onMessage: { + addListener: (listener) => { + foregroundMessageListeners.current.push(listener) + }, + removeListener: (listener) => { + foregroundMessageListeners.current.splice( + foregroundMessageListeners.current.indexOf(listener), + 1, + ) + }, + }, + onDisconnect: { + addListener: () => {}, + removeListener: () => {}, + }, + } + const bingToken = (await getUserConfig()).bingAccessToken + if (session.modelName.includes('bingFreeSydney')) + await generateAnswersWithBingWebApi(fakePort, session.question, session, bingToken, true) + else await generateAnswersWithBingWebApi(fakePort, session.question, session, bingToken) + } + } else { + port.postMessage({ session, stop }) + } + } + useEffect(() => { const portListener = () => { setPort(Browser.runtime.connect()) @@ -146,68 +242,17 @@ function ConversationCard(props) { } }, [port]) useEffect(() => { - const listener = (msg) => { - if (msg.answer) { - updateAnswer(msg.answer, false, 'answer') - } - if (msg.session) { - if (msg.done) msg.session = { ...msg.session, isRetry: false } - setSession(msg.session) + if (useForegroundFetch) { + return () => {} + } else { + port.onMessage.addListener(portMessageListener) + return () => { + port.onMessage.removeListener(portMessageListener) } - if (msg.done) { - updateAnswer('', true, 'answer', true) - setIsReady(true) - } - if (msg.error) { - switch (msg.error) { - case 'UNAUTHORIZED': - updateAnswer( - `${t('UNAUTHORIZED')}
${t('Please login at https://chat.openai.com first')}${ - isSafari() ? `
${t('Then open https://chat.openai.com/api/auth/session')}` : '' - }
${t('And refresh this page or type you question again')}` + - `

${t( - 'Consider creating an api key at https://platform.openai.com/account/api-keys', - )}`, - false, - 'error', - ) - break - case 'CLOUDFLARE': - updateAnswer( - `${t('OpenAI Security Check Required')}
${ - isSafari() - ? t('Please open https://chat.openai.com/api/auth/session') - : t('Please open https://chat.openai.com') - }
${t('And refresh this page or type you question again')}` + - `

${t( - 'Consider creating an api key at https://platform.openai.com/account/api-keys', - )}`, - false, - 'error', - ) - break - default: - if ( - conversationItemData[conversationItemData.length - 1].content.includes('gpt-loading') - ) - updateAnswer(msg.error, false, 'error') - else - setConversationItemData([ - ...conversationItemData, - new ConversationItemData('error', msg.error), - ]) - break - } - setIsReady(true) - } - } - port.onMessage.addListener(listener) - return () => { - port.onMessage.removeListener(listener) } }, [conversationItemData]) - const getRetryFn = (session) => () => { + const getRetryFn = (session) => async () => { updateAnswer(`

${t('Waiting for response...')}

`, false, 'answer') setIsReady(false) @@ -223,8 +268,8 @@ function ConversationCard(props) { const newSession = { ...session, isRetry: true } setSession(newSession) try { - port.postMessage({ stop: true }) - port.postMessage({ session: newSession }) + await postMessage({ stop: true }) + await postMessage({ session: newSession }) } catch (e) { updateAnswer(e, false, 'error') } @@ -348,8 +393,8 @@ function ConversationCard(props) { { - port.postMessage({ stop: true }) + onConfirm={async () => { + await postMessage({ stop: true }) Browser.runtime.sendMessage({ type: 'DELETE_CONVERSATION', data: { @@ -449,7 +494,7 @@ function ConversationCard(props) { enabled={isReady} port={port} reverseResizeDir={props.pageMode} - onSubmit={(question) => { + onSubmit={async (question) => { const newQuestion = new ConversationItemData('question', question) const newAnswer = new ConversationItemData( 'answer', @@ -461,7 +506,7 @@ function ConversationCard(props) { const newSession = { ...session, question, isRetry: false } setSession(newSession) try { - port.postMessage({ session: newSession }) + await postMessage({ session: newSession }) } catch (e) { updateAnswer(e, false, 'error') } diff --git a/src/config/index.mjs b/src/config/index.mjs index d7fd3d2f..104c4745 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -207,6 +207,7 @@ export const defaultConfig = { ], accessToken: '', tokenSavedOn: 0, + bingAccessToken: '', chatgptJumpBackTabId: 0, chatgptTabId: 0, diff --git a/src/manifest.json b/src/manifest.json index bad43fd4..dca826e1 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -10,10 +10,13 @@ "128": "logo.png" }, "host_permissions": [ - "https://*.openai.com/", - "https://*.bing.com/", - "https://*.poe.com/", - "https://*.google.com/" + "https://*.openai.com/*", + "https://*.bing.com/*", + "wss://*.bing.com/*", + "https://*.poe.com/*", + "https://*.google.com/*", + "https://claude.ai/*", + "" ], "permissions": [ "commands", @@ -21,7 +24,9 @@ "storage", "contextMenus", "unlimitedStorage", - "tabs" + "tabs", + "webRequest", + "declarativeNetRequestWithHostAccess" ], "optional_permissions": [ "background" @@ -32,6 +37,15 @@ "action": { "default_popup": "popup.html" }, + "declarative_net_request": { + "rule_resources": [ + { + "id": "ruleset", + "enabled": true, + "path": "rules.json" + } + ] + }, "options_ui": { "page": "popup.html", "open_in_tab": true diff --git a/src/manifest.v2.json b/src/manifest.v2.json index 11f49d4e..d9a51ab8 100644 --- a/src/manifest.v2.json +++ b/src/manifest.v2.json @@ -16,10 +16,14 @@ "contextMenus", "unlimitedStorage", "tabs", + "webRequest", "https://*.openai.com/", "https://*.bing.com/", + "wss://*.bing.com/*", "https://*.poe.com/", - "https://*.google.com/" + "https://*.google.com/", + "https://claude.ai/", + "" ], "background": { "scripts": [ diff --git a/src/rules.json b/src/rules.json new file mode 100644 index 00000000..01949616 --- /dev/null +++ b/src/rules.json @@ -0,0 +1,24 @@ +[ + { + "id": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "operation": "set", + "header": "origin", + "value": "https://www.bing.com" + }, + { + "operation": "set", + "header": "referer", + "value": "https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx" + } + ] + }, + "condition": { + "requestDomains": ["sydney.bing.com", "www.bing.com"], + "resourceTypes": ["xmlhttprequest", "websocket"] + } + } +] diff --git a/src/services/clients/bing/index.mjs b/src/services/clients/bing/index.mjs index 3abea466..a780147c 100644 --- a/src/services/clients/bing/index.mjs +++ b/src/services/clients/bing/index.mjs @@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid' import BingImageCreator from './BingImageCreator' +import { fetchBg } from '../../../utils/fetch-bg.mjs' /** * https://stackoverflow.com/a/58326357 @@ -71,51 +72,53 @@ export default class BingAIClient { } async createNewConversation() { - const fetchOptions = { - headers: { - accept: 'application/json', - 'accept-language': 'en-US,en;q=0.9', - 'content-type': 'application/json', - '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.50"', - '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"', - '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': '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', - '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) { - // fetchOptions.dispatcher = new ProxyAgent(this.options.proxy); + this.headers = { + accept: 'application/json', + 'accept-language': 'en-US,en;q=0.9', + 'content-type': 'application/json', + '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.50"', + '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"', + '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': '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', + '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 } : {}), } - const response = await fetch(`${this.options.host}/turing/conversation/create`, fetchOptions) + // filter undefined values + this.headers = Object.fromEntries( + Object.entries(this.headers).filter(([, value]) => value !== undefined), + ) - const { status, headers } = response - if (status === 200 && +headers.get('content-length') < 5) { - throw new Error('/turing/conversation/create: Your IP is blocked by BingAI.') + const fetchOptions = { + headers: this.headers, } - + // if (this.options.proxy) { + // fetchOptions.dispatcher = new ProxyAgent(this.options.proxy) + // } else { + // fetchOptions.dispatcher = new Agent({ connect: { timeout: 20_000 } }) + // } + const response = await fetchBg(`${this.options.host}/turing/conversation/create`, fetchOptions) const body = await response.text() try { return JSON.parse(body) @@ -126,10 +129,10 @@ export default class BingAIClient { async createWebSocketConnection() { return new Promise((resolve, reject) => { - // let agent; - if (this.options.proxy) { - // agent = new HttpsProxyAgent(this.options.proxy); - } + // let agent + // if (this.options.proxy) { + // agent = new HttpsProxyAgent(this.options.proxy) + // } const ws = new WebSocket('wss://sydney.bing.com/sydney/ChatHub') @@ -449,6 +452,9 @@ export default class BingAIClient { if (!messages?.length || messages[0].author !== 'bot') { return } + if (messages[0].contentOrigin === 'Apology') { + return + } if (messages[0]?.contentType === 'IMAGE') { // You will never get a message of this type without 'gencontentv3' being on. bicIframe = this.bic diff --git a/src/utils/fetch-bg.mjs b/src/utils/fetch-bg.mjs new file mode 100644 index 00000000..83e0bf5b --- /dev/null +++ b/src/utils/fetch-bg.mjs @@ -0,0 +1,30 @@ +import Browser from 'webextension-polyfill' + +/** + * @param {RequestInfo|URL} input + * @param {RequestInit=} init + * @returns {Promise} + */ +export function fetchBg(input, init) { + return new Promise((resolve, reject) => { + Browser.runtime + .sendMessage({ + type: 'FETCH', + data: { input, init }, + }) + .then((messageResponse) => { + const [response, error] = messageResponse + if (response === null) { + reject(error) + } else { + const body = response.body ? new Blob([response.body]) : undefined + resolve( + new Response(body, { + status: response.status, + statusText: response.statusText, + }), + ) + } + }) + }) +}