From 6ad42461a2656c071ba54ae1dff88a0f84148e50 Mon Sep 17 00:00:00 2001 From: weaigc <879821485@qq.com> Date: Mon, 21 Aug 2023 20:20:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat-image.tsx | 12 +--- src/lib/bots/bing/index.ts | 22 ------ src/lib/hooks/use-bing.ts | 2 +- src/lib/image.ts | 131 ++++++++++++++++++++++++++++++++++ src/lib/utils.ts | 10 +-- src/pages/api/blob.ts | 13 ++-- src/pages/api/create.ts | 2 +- src/pages/api/image.ts | 4 +- src/pages/api/kblob.ts | 25 ++++--- src/pages/api/proxy.ts | 5 +- tests/kblob.ts | 33 ++++++--- 11 files changed, 190 insertions(+), 69 deletions(-) create mode 100644 src/lib/image.ts diff --git a/src/components/chat-image.tsx b/src/components/chat-image.tsx index c914febe..6c4a8f57 100644 --- a/src/components/chat-image.tsx +++ b/src/components/chat-image.tsx @@ -9,12 +9,13 @@ import { KeyboardEvent } from "react" import Image from 'next/image' +import { toast } from "react-hot-toast" import PasteIcon from '@/assets/images/paste.svg' import UploadIcon from '@/assets/images/upload.svg' import CameraIcon from '@/assets/images/camera.svg' import { useBing } from '@/lib/hooks/use-bing' import { cn } from '@/lib/utils' -import { toast } from "react-hot-toast" +import { ImageUtils } from "@/lib/image" interface ChatImageProps extends Pick, 'uploadImage'> {} @@ -22,13 +23,6 @@ const preventDefault: MouseEventHandler = (event) => { event.nativeEvent.stopImmediatePropagation() } -const toBase64 = (file: File): Promise => new Promise((resolve, reject) => { - const reader = new FileReader() - reader.readAsDataURL(file) - reader.onload = () => resolve(reader.result as string) - reader.onerror = reject -}) - export function ChatImage({ children, uploadImage }: React.PropsWithChildren) { const videoRef = useRef(null) const canvasRef = useRef(null) @@ -46,7 +40,7 @@ export function ChatImage({ children, uploadImage }: React.PropsWithChildren) => { const file = event.target.files?.[0] if (file) { - const fileDataUrl = await toBase64(file) + const fileDataUrl = await ImageUtils.getCompressedImageDataAsync(file) if (fileDataUrl) { upload(fileDataUrl) } diff --git a/src/lib/bots/bing/index.ts b/src/lib/bots/bing/index.ts index c75c69f9..4596f9de 100644 --- a/src/lib/bots/bing/index.ts +++ b/src/lib/bots/bing/index.ts @@ -249,28 +249,6 @@ export class BingWebBot { return wsp } - private async useWs(params: Params) { - const wsp = await this.sendWs() - const watchDog = new WatchDog() - wsp.onUnpackedMessage.addListener((events) => { - watchDog.watch(() => { - wsp.sendPacked({ type: 6 }) - }) - this.parseEvents(params, events) - }) - - wsp.onClose.addListener(() => { - watchDog.reset() - params.onEvent({ type: 'DONE' }) - wsp.removeAllListeners() - }) - - params.signal?.addEventListener('abort', () => { - wsp.removeAllListeners() - wsp.close() - }) - } - private async createImage(prompt: string, id: string) { try { const headers = { diff --git a/src/lib/hooks/use-bing.ts b/src/lib/hooks/use-bing.ts index dcdb1667..b9361916 100644 --- a/src/lib/hooks/use-bing.ts +++ b/src/lib/hooks/use-bing.ts @@ -47,7 +47,7 @@ export function useBing(botId: BotId = 'bing') { speaker.reset() await chatState.bot.sendMessage({ prompt: input, - imageUrl: /\?bcid=([^&]+)/.test(imageUrl ?? '') ? `https://www.bing.com/images/blob?bcid=${RegExp.$1}` : imageUrl, + imageUrl: imageUrl ? new URL(imageUrl, location.href).toString() : undefined, options: { ...options, bingConversationStyle, diff --git a/src/lib/image.ts b/src/lib/image.ts new file mode 100644 index 00000000..4ca4a954 --- /dev/null +++ b/src/lib/image.ts @@ -0,0 +1,131 @@ +export class FileUtils { + static getDataUrlAsync(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onabort = () => { + reject('Load Aborted') + } + + reader.onerror = () => { + reject(reader.error) + } + + reader.onload = () => { + resolve(reader.result as string) + } + + reader.readAsDataURL(file) + }) + } + static getArrayBufferAsync(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onabort = () => { + reject('Load Aborted') + } + + reader.onerror = () => { + reject(reader.error) + } + + reader.onload = () => { + resolve(reader.result as ArrayBuffer) + } + + reader.readAsArrayBuffer(file) + }) + } + static getImageElementFromDataUrl(dataURL: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image() + image.onerror = () => { + reject(null) + } + + image.onload = () => { + resolve(image) + } + + image.src = dataURL + }) + } + + static dataURLtoFile(dataURL: string, type?: string) { + const typeRe = /:(.*?);/ + let [meta, base64] = dataURL.split(',') + if (!base64) { + base64 = meta + } else if (!type) { + type = typeRe.test(meta) ? RegExp.$1 : undefined + } + + const rawData = atob(base64); + let len = rawData.length; + const unitArray = new Uint8Array(len); + for (; len--;) + unitArray[len] = rawData.charCodeAt(len); + return new File([unitArray], 'temp', type ? { + type + } : undefined) + } + static isFile(file: File | string) { + return Boolean((file as File).stream) + } +} + +export class ImageUtils { + static async getCompressedImageDataAsync(file: File | string) { + let dataURI: string + let fileObj: File + if (FileUtils.isFile(file)) { + fileObj = file as File + dataURI = await FileUtils.getDataUrlAsync(fileObj) + } else { + dataURI = file as string + fileObj = FileUtils.dataURLtoFile(dataURI) + } + + if (typeof document === 'undefined' || !document.createElement) { + return dataURI + } + const image = await FileUtils.getImageElementFromDataUrl(dataURI) + if (!image.width || !image.height) + throw new Error('Failed to load image.') + + let { width, height } = image + const rate = 36e4 / (width * height); + if (rate < 1) { + const scaleRate = Math.sqrt(rate); + width *= scaleRate, + height *= scaleRate + } + + const canvas = document.createElement('canvas'); + ImageUtils.processImage(canvas, width, height, image) + return ImageUtils.getImageDataOnCanvas(canvas) + } + + static drawImageOnCanvas(canvas: HTMLCanvasElement, image: HTMLImageElement, width: number, height: number) { + const ctx = canvas.getContext('2d'); + if (ctx) { + canvas.width = width, + canvas.height = height, + ctx.drawImage(image, 0, 0, width, height) + } + } + static getImageDataOnCanvas(canvas: HTMLCanvasElement) { + return canvas.toDataURL('image/jpeg', 0.7) + } + static processImage(canvas: HTMLCanvasElement, targetWidth: number, targetHeight: number, image: HTMLImageElement) { + const { width, height } = canvas.style + const ctx = canvas.getContext('2d')! + canvas.width = targetWidth + canvas.height = targetHeight + canvas.style.width = width + canvas.style.height = height + + ctx.fillStyle = '#FFFFFF' + ctx.fillRect(0, 0, targetWidth, targetHeight) + ctx.drawImage(image, 0, 0, targetWidth, targetHeight) + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8de2eba9..77f30fab 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -109,13 +109,13 @@ export function parseUA(ua?: string, default_ua = DEFAULT_UA) { export function mockUser(cookies: Partial<{ [key: string]: string }>) { const { BING_UA = process.env.BING_UA, - BING_IP, + BING_IP = '', _U = defaultUID, } = cookies const ua = parseUA(BING_UA) return { - 'x-forwarded-for': BING_IP!, + 'x-forwarded-for': BING_IP, 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'User-Agent': ua!, @@ -124,10 +124,10 @@ export function mockUser(cookies: Partial<{ [key: string]: string }>) { } } -export function createHeaders(cookies: Partial<{ [key: string]: string }>, type?: string) { +export function createHeaders(cookies: Partial<{ [key: string]: string }>, type?: 'image') { let { BING_HEADER = process.env.BING_HEADER, - BING_IP, + BING_IP = '', IMAGE_ONLY = process.env.IMAGE_ONLY ?? '1', } = cookies const imageOnly = /^(1|true|yes)$/.test(String(IMAGE_ONLY)) @@ -140,7 +140,7 @@ export function createHeaders(cookies: Partial<{ [key: string]: string }>, type? BING_HEADER, ...cookies, }) || {} - headers['x-forward-for'] = BING_IP! + headers['x-forward-for'] = BING_IP return headers } } diff --git a/src/pages/api/blob.ts b/src/pages/api/blob.ts index fecd4803..94ae0934 100644 --- a/src/pages/api/blob.ts +++ b/src/pages/api/blob.ts @@ -3,22 +3,17 @@ import { NextApiRequest, NextApiResponse } from 'next' import { Readable } from 'node:stream' import { fetch } from '@/lib/isomorphic' +import { createHeaders } from '@/lib/utils' const API_DOMAIN = 'https://www.bing.com' export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { bcid } = req.query - const { headers, body } = await fetch(`${API_DOMAIN}/images/blob?bcid=${bcid}`, { method: 'GET', - headers: { - "sec-ch-ua": "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"115\", \"Chromium\";v=\"115\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "Referrer-Policy": "origin-when-cross-origin", - }, + headers: createHeaders(req.cookies, 'image'), }, ) @@ -27,10 +22,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) 'Content-Type': headers.get('content-type')!, }) // @ts-ignore - return Readable.fromWeb(body!).pipe(res) + Readable.fromWeb(body!).pipe(res) } catch (e) { console.log('Error', e) - return res.json({ + res.json({ result: { value: 'UploadFailed', message: `${e}` diff --git a/src/pages/api/create.ts b/src/pages/api/create.ts index e44581b1..d534e75d 100644 --- a/src/pages/api/create.ts +++ b/src/pages/api/create.ts @@ -37,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) })) } catch (e) { console.log('error', e) - return res.end(JSON.stringify({ + res.end(JSON.stringify({ result: { value: 'UnauthorizedRequest', message: `${e}` diff --git a/src/pages/api/image.ts b/src/pages/api/image.ts index 26fdb310..12e8ce38 100644 --- a/src/pages/api/image.ts +++ b/src/pages/api/image.ts @@ -26,9 +26,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.writeHead(200, { 'Content-Type': 'text/plain; charset=UTF-8', }) - return res.end(response) + res.end(response) } catch (e) { - return res.json({ + res.json({ result: { value: 'Error', message: `${e}` diff --git a/src/pages/api/kblob.ts b/src/pages/api/kblob.ts index 0ce7e606..ccd808fd 100644 --- a/src/pages/api/kblob.ts +++ b/src/pages/api/kblob.ts @@ -2,10 +2,11 @@ import { NextApiRequest, NextApiResponse } from 'next' import FormData from 'form-data' -import { fetch } from '@/lib/isomorphic' +import { debug, fetch } from '@/lib/isomorphic' import { KBlobRequest } from '@/lib/bots/bing/types' +import { createHeaders } from '@/lib/utils' -const API_DOMAIN = 'https://bing.vcanbb.top' +const API_DOMAIN = 'https://www.bing.com' export const config = { api: { @@ -18,6 +19,7 @@ export const config = { export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { knowledgeRequest, imageBase64 } = req.body as KBlobRequest + const headers = createHeaders(req.cookies, 'image') const formData = new FormData() formData.append('knowledgeRequest', JSON.stringify(knowledgeRequest)) @@ -30,23 +32,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) method: 'POST', body: formData.getBuffer(), headers: { - "sec-ch-ua": "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"115\", \"Chromium\";v=\"115\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "Referer": `${API_DOMAIN}/web/index.html`, - "Referrer-Policy": "origin-when-cross-origin", - 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32', + 'x-forward-for': headers['x-forwarded-for'], + 'user-agent': headers['User-Agent'], + cookie: headers['cookie'], + 'Referer': 'https://www.bing.com/search', ...formData.getHeaders() } } - ).then(res => res.text()) + ) + if (response.status !== 200) { + throw new Error('图片上传失败') + } res.writeHead(200, { 'Content-Type': 'application/json', }) - res.end(response || JSON.stringify({ result: { value: 'UploadFailed', message: '请更换 IP 或代理后重试' } })) + res.end(await response.text()) } catch (e) { - return res.json({ + res.json({ result: { value: 'UploadFailed', message: `${e}` diff --git a/src/pages/api/proxy.ts b/src/pages/api/proxy.ts index 240b5fb5..a70352bc 100644 --- a/src/pages/api/proxy.ts +++ b/src/pages/api/proxy.ts @@ -6,9 +6,11 @@ import { fetch } from '@/lib/isomorphic' export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { const { url, headers, method = 'GET', body } = req.body + console.log(req.body) if (!url) { return res.end('ok') } + console.log(method, url, headers, body) const response = await fetch(url, { headers, method, body, redirect: 'manual' }) const text = await response.text() res.writeHead(200, { @@ -19,6 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.end(text) } catch (e) { console.log(e) - return res.end(e) + res.end(String(e)) + return } } diff --git a/tests/kblob.ts b/tests/kblob.ts index 9e15b41c..50d48ea2 100644 --- a/tests/kblob.ts +++ b/tests/kblob.ts @@ -4,24 +4,41 @@ import { fetch } from '@/lib/isomorphic' const formData = new FormData() -const knowledgeRequest = {"imageInfo":{"url":"https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png"},"knowledgeRequest":{"invokedSkills":["ImageById"],"subscriptionId":"Bing.Chat.Multimodal","invokedSkillsRequestData":{"enableFaceBlur":true},"convoData":{"convoid":"51D|BingProdUnAuthenticatedUsers|E3DCA904FF236C67C3450163BCEC64CFF3F618CC8A4AFD75FD518F5ED0ADA080","convotone":"Creative"}}} +const knowledgeRequest = { + "imageInfo": { + "url": "https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png" + }, + "knowledgeRequest": { + "invokedSkills": ["ImageById"], + "subscriptionId": "Bing.Chat.Multimodal", + "invokedSkillsRequestData": { "enableFaceBlur": true }, + "convoData": { "convoid": "51D|BingProdUnAuthenticatedUsers|E3DCA904FF236C67C3450163BCEC64CFF3F618CC8A4AFD75FD518F5ED0ADA080", + "convotone": "Creative" } + } +} formData.append('knowledgeRequest', JSON.stringify(knowledgeRequest)) -fetch('https://bing.vcanbb.top/images/kblob', +const jsonData = { + "imageInfo": { "url": "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png?1=1" }, + "knowledgeRequest": { + "invokedSkills": ["ImageById"], + "subscriptionId": "Bing.Chat.Multimodal", + "invokedSkillsRequestData": { "enableFaceBlur": true }, + "convoData": { "convoid": "", "convotone": "Creative" } + } +} + +fetch('https://www.bing.com/images/kblob', { method: 'POST', body: formData.getBuffer(), headers: { - "sec-ch-ua": "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"115\", \"Chromium\";v=\"115\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "Referer": "https://bing.vcanbb.top/web/index.html", - "Referrer-Policy": "origin-when-cross-origin", + 'Referer': 'https://www.bing.com/search', ...formData.getHeaders() } } ).then(res => res.text()) -.then(res => console.log('res', res)) + .then(res => console.log('res', res))