mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-29 09:00:18 +09:00
feat: 1. using cache storage store image data; 2. get base64image before chat to api #5013
This commit is contained in:
parent
afa1a4303b
commit
287fa0a39c
@ -21,7 +21,7 @@ import {
|
|||||||
} from "@fortaine/fetch-event-source";
|
} from "@fortaine/fetch-event-source";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
import Locale from "../../locales";
|
import Locale from "../../locales";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||||
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
|
|
||||||
export type MultiBlockContent = {
|
export type MultiBlockContent = {
|
||||||
@ -93,7 +94,12 @@ export class ClaudeApi implements LLMApi {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = [...options.messages];
|
// try get base64image from local cache image_url
|
||||||
|
const messages = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = await preProcessImageContent(v.content);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
const keys = ["system", "user"];
|
const keys = ["system", "user"];
|
||||||
|
|
||||||
@ -135,6 +141,7 @@ export class ClaudeApi implements LLMApi {
|
|||||||
content: content
|
content: content
|
||||||
.filter((v) => v.image_url || v.text)
|
.filter((v) => v.image_url || v.text)
|
||||||
.map(({ type, text, image_url }) => {
|
.map(({ type, text, image_url }) => {
|
||||||
|
console.log("process message", type, text, image_url);
|
||||||
if (type === "text") {
|
if (type === "text") {
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
|
|
||||||
export class GeminiProApi implements LLMApi {
|
export class GeminiProApi implements LLMApi {
|
||||||
path(path: string): string {
|
path(path: string): string {
|
||||||
@ -56,7 +57,14 @@ export class GeminiProApi implements LLMApi {
|
|||||||
async chat(options: ChatOptions): Promise<void> {
|
async chat(options: ChatOptions): Promise<void> {
|
||||||
const apiClient = this;
|
const apiClient = this;
|
||||||
let multimodal = false;
|
let multimodal = false;
|
||||||
const messages = options.messages.map((v) => {
|
|
||||||
|
// try get base64image from local cache image_url
|
||||||
|
const _messages = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = await preProcessImageContent(v.content);
|
||||||
|
_messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
const messages = _messages.map((v) => {
|
||||||
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
||||||
if (isVisionModel(options.config.model)) {
|
if (isVisionModel(options.config.model)) {
|
||||||
const images = getMessageImages(v);
|
const images = getMessageImages(v);
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/app/constant";
|
} from "@/app/constant";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
||||||
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -105,10 +106,13 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
|
|
||||||
async chat(options: ChatOptions) {
|
async chat(options: ChatOptions) {
|
||||||
const visionModel = isVisionModel(options.config.model);
|
const visionModel = isVisionModel(options.config.model);
|
||||||
const messages = options.messages.map((v) => ({
|
const messages = [];
|
||||||
role: v.role,
|
for (const v of options.messages) {
|
||||||
content: visionModel ? v.content : getMessageTextContent(v),
|
const content = visionModel
|
||||||
}));
|
? await preProcessImageContent(v.content)
|
||||||
|
: getMessageTextContent(v);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
const modelConfig = {
|
const modelConfig = {
|
||||||
...useAppConfig.getState().modelConfig,
|
...useAppConfig.getState().modelConfig,
|
||||||
|
@ -61,7 +61,7 @@ import {
|
|||||||
isVisionModel,
|
isVisionModel,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
import { compressImage } from "@/app/utils/chat";
|
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
@ -1167,7 +1167,7 @@ function _Chat() {
|
|||||||
...(await new Promise<string[]>((res, rej) => {
|
...(await new Promise<string[]>((res, rej) => {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
const imagesData: string[] = [];
|
const imagesData: string[] = [];
|
||||||
compressImage(file, 256 * 1024)
|
uploadImageRemote(file)
|
||||||
.then((dataUrl) => {
|
.then((dataUrl) => {
|
||||||
imagesData.push(dataUrl);
|
imagesData.push(dataUrl);
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
@ -1209,7 +1209,7 @@ function _Chat() {
|
|||||||
const imagesData: string[] = [];
|
const imagesData: string[] = [];
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = event.target.files[i];
|
const file = event.target.files[i];
|
||||||
compressImage(file, 256 * 1024)
|
uploadImageRemote(file)
|
||||||
.then((dataUrl) => {
|
.then((dataUrl) => {
|
||||||
imagesData.push(dataUrl);
|
imagesData.push(dataUrl);
|
||||||
if (
|
if (
|
||||||
|
@ -21,6 +21,9 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
|
|||||||
|
|
||||||
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
|
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
|
||||||
|
|
||||||
|
export const CACHE_URL_PREFIX = "/api/cache";
|
||||||
|
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||||
|
|
||||||
export enum Path {
|
export enum Path {
|
||||||
Home = "/",
|
Home = "/",
|
||||||
Chat = "/chat",
|
Chat = "/chat",
|
||||||
@ -239,7 +242,7 @@ const baiduModels = [
|
|||||||
"ernie-speed-128k",
|
"ernie-speed-128k",
|
||||||
"ernie-speed-8k",
|
"ernie-speed-8k",
|
||||||
"ernie-lite-8k",
|
"ernie-lite-8k",
|
||||||
"ernie-tiny-8k"
|
"ernie-tiny-8k",
|
||||||
];
|
];
|
||||||
|
|
||||||
const bytedanceModels = [
|
const bytedanceModels = [
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import heic2any from "heic2any";
|
import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant";
|
||||||
|
// import heic2any from "heic2any";
|
||||||
|
|
||||||
export function compressImage(file: File, maxSize: number): Promise<string> {
|
export function compressImage(file: File, maxSize: number): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -40,6 +41,7 @@ export function compressImage(file: File, maxSize: number): Promise<string> {
|
|||||||
reader.onerror = reject;
|
reader.onerror = reject;
|
||||||
|
|
||||||
if (file.type.includes("heic")) {
|
if (file.type.includes("heic")) {
|
||||||
|
const heic2any = require("heic2any");
|
||||||
heic2any({ blob: file, toType: "image/jpeg" })
|
heic2any({ blob: file, toType: "image/jpeg" })
|
||||||
.then((blob) => {
|
.then((blob) => {
|
||||||
reader.readAsDataURL(blob as Blob);
|
reader.readAsDataURL(blob as Blob);
|
||||||
@ -52,3 +54,78 @@ export function compressImage(file: File, maxSize: number): Promise<string> {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function preProcessImageContent(
|
||||||
|
content: RequestMessage["content"],
|
||||||
|
) {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
const result = [];
|
||||||
|
for (const part of content) {
|
||||||
|
if (part?.type == "image_url" && part?.image_url?.url) {
|
||||||
|
const url = await cacheImageToBase64Image(part?.image_url?.url);
|
||||||
|
result.push({ type: part.type, image_url: { url } });
|
||||||
|
} else {
|
||||||
|
result.push({ ...part });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageCaches = {};
|
||||||
|
export function cacheImageToBase64Image(imageUrl: string) {
|
||||||
|
if (imageUrl.includes(CACHE_URL_PREFIX)) {
|
||||||
|
if (!imageCaches[imageUrl]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
return fetch(imageUrl, {
|
||||||
|
method: "GET",
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((res) => res.blob())
|
||||||
|
.then(
|
||||||
|
(blob) => (imageCaches[imageUrl] = compressImage(blob, 256 * 1024)),
|
||||||
|
); // compressImage
|
||||||
|
}
|
||||||
|
return Promise.resolve(imageCaches[imageUrl]);
|
||||||
|
}
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64Image2Blob(base64Data: string, contentType: string) {
|
||||||
|
const byteCharacters = atob(base64Data);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
return new Blob([byteArray], { type: contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadImage(file: File): Promise<string> {
|
||||||
|
const body = new FormData();
|
||||||
|
body.append("file", file);
|
||||||
|
return fetch(UPLOAD_URL, {
|
||||||
|
method: "post",
|
||||||
|
body,
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
console.log("res", res);
|
||||||
|
if (res?.code == 0 && res?.data) {
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
throw Error(`upload Error: ${res?.msg}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeImage(imageUrl: string) {
|
||||||
|
return fetch(imageUrl, {
|
||||||
|
method: "DELETE",
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
|
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
|
||||||
|
const CHATGPT_NEXT_WEB_FILE_CACHE = "chatgpt-next-web-file";
|
||||||
|
let a="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let nanoid=(e=21)=>{let t="",r=crypto.getRandomValues(new Uint8Array(e));for(let n=0;n<e;n++)t+=a[63&r[n]];return t};
|
||||||
|
|
||||||
self.addEventListener("activate", function (event) {
|
self.addEventListener("activate", function (event) {
|
||||||
console.log("ServiceWorker activated.");
|
console.log("ServiceWorker activated.");
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("install", function (event) {
|
self.addEventListener("install", function (event) {
|
||||||
|
self.skipWaiting(); // enable new version
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
|
caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
|
||||||
return cache.addAll([]);
|
return cache.addAll([]);
|
||||||
@ -12,4 +15,45 @@ self.addEventListener("install", function (event) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", (e) => {});
|
async function upload(request, url) {
|
||||||
|
const formData = await request.formData()
|
||||||
|
const file = formData.getAll('file')[0]
|
||||||
|
let ext = file.name.split('.').pop()
|
||||||
|
if (ext === 'blob') {
|
||||||
|
ext = file.type.split('/').pop()
|
||||||
|
}
|
||||||
|
const fileUrl = `${url.origin}/api/cache/${nanoid()}.${ext}`
|
||||||
|
// console.debug('file', file, fileUrl, request)
|
||||||
|
const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
|
||||||
|
await cache.put(new Request(fileUrl), new Response(file, {
|
||||||
|
headers: {
|
||||||
|
'content-type': file.type,
|
||||||
|
'content-length': file.size,
|
||||||
|
'cache-control': 'no-cache', // file already store in disk
|
||||||
|
'server': 'ServiceWorker',
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
return Response.json({ code: 0, data: fileUrl })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(request, url) {
|
||||||
|
const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
|
||||||
|
const res = await cache.delete(request.url)
|
||||||
|
return Response.json({ code: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (e) => {
|
||||||
|
const url = new URL(e.request.url);
|
||||||
|
if (/^\/api\/cache/.test(url.pathname)) {
|
||||||
|
if ('GET' == e.request.method) {
|
||||||
|
e.respondWith(caches.match(e.request))
|
||||||
|
}
|
||||||
|
if ('POST' == e.request.method) {
|
||||||
|
e.respondWith(upload(e.request, url))
|
||||||
|
}
|
||||||
|
if ('DELETE' == e.request.method) {
|
||||||
|
e.respondWith(remove(e.request, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@ -2,8 +2,15 @@ if ('serviceWorker' in navigator) {
|
|||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) {
|
navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) {
|
||||||
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
||||||
|
registration.update().then(res => {
|
||||||
|
console.log('ServiceWorker registration update: ', res);
|
||||||
|
});
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
console.error('ServiceWorker registration failed: ', err);
|
console.error('ServiceWorker registration failed: ', err);
|
||||||
});
|
});
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', function() {
|
||||||
|
console.log('ServiceWorker controllerchange ');
|
||||||
|
window.location.reload(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user