Skip to content

Commit 9a78a72

Browse files
authored
Merge pull request #5061 from ConnectAI-E/feature-cache-storage
using cache storage store image data #5013
2 parents 12cad4c + 862c2e8 commit 9a78a72

9 files changed

Lines changed: 196 additions & 24 deletions

File tree

app/client/platforms/alibaba.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from "@fortaine/fetch-event-source";
2222
import { prettyObject } from "@/app/utils/format";
2323
import { getClientConfig } from "@/app/config/client";
24-
import { getMessageTextContent, isVisionModel } from "@/app/utils";
24+
import { getMessageTextContent } from "@/app/utils";
2525

2626
export interface OpenAIListModelResponse {
2727
object: string;

app/client/platforms/anthropic.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
33
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
44
import { getClientConfig } from "@/app/config/client";
55
import { DEFAULT_API_HOST } from "@/app/constant";
6-
import { RequestMessage } from "@/app/typing";
76
import {
87
EventStreamContentType,
98
fetchEventSource,
@@ -12,6 +11,7 @@ import {
1211
import Locale from "../../locales";
1312
import { prettyObject } from "@/app/utils/format";
1413
import { getMessageTextContent, isVisionModel } from "@/app/utils";
14+
import { preProcessImageContent } from "@/app/utils/chat";
1515
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
1616

1717
export type MultiBlockContent = {
@@ -93,7 +93,12 @@ export class ClaudeApi implements LLMApi {
9393
},
9494
};
9595

96-
const messages = [...options.messages];
96+
// try get base64image from local cache image_url
97+
const messages: ChatOptions["messages"] = [];
98+
for (const v of options.messages) {
99+
const content = await preProcessImageContent(v.content);
100+
messages.push({ role: v.role, content });
101+
}
97102

98103
const keys = ["system", "user"];
99104

app/client/platforms/google.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getMessageImages,
1515
isVisionModel,
1616
} from "@/app/utils";
17+
import { preProcessImageContent } from "@/app/utils/chat";
1718

1819
export class GeminiProApi implements LLMApi {
1920
path(path: string): string {
@@ -56,7 +57,14 @@ export class GeminiProApi implements LLMApi {
5657
async chat(options: ChatOptions): Promise<void> {
5758
const apiClient = this;
5859
let multimodal = false;
59-
const messages = options.messages.map((v) => {
60+
61+
// try get base64image from local cache image_url
62+
const _messages: ChatOptions["messages"] = [];
63+
for (const v of options.messages) {
64+
const content = await preProcessImageContent(v.content);
65+
_messages.push({ role: v.role, content });
66+
}
67+
const messages = _messages.map((v) => {
6068
let parts: any[] = [{ text: getMessageTextContent(v) }];
6169
if (isVisionModel(options.config.model)) {
6270
const images = getMessageImages(v);

app/client/platforms/openai.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "@/app/constant";
1212
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
1313
import { collectModelsWithDefaultModel } from "@/app/utils/model";
14+
import { preProcessImageContent } from "@/app/utils/chat";
1415
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
1516

1617
import {
@@ -105,10 +106,13 @@ export class ChatGPTApi implements LLMApi {
105106

106107
async chat(options: ChatOptions) {
107108
const visionModel = isVisionModel(options.config.model);
108-
const messages = options.messages.map((v) => ({
109-
role: v.role,
110-
content: visionModel ? v.content : getMessageTextContent(v),
111-
}));
109+
const messages: ChatOptions["messages"] = [];
110+
for (const v of options.messages) {
111+
const content = visionModel
112+
? await preProcessImageContent(v.content)
113+
: getMessageTextContent(v);
114+
messages.push({ role: v.role, content });
115+
}
112116

113117
const modelConfig = {
114118
...useAppConfig.getState().modelConfig,

app/components/chat.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import {
6161
isVisionModel,
6262
} from "../utils";
6363

64-
import { compressImage } from "@/app/utils/chat";
64+
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
6565

6666
import dynamic from "next/dynamic";
6767

@@ -1167,7 +1167,7 @@ function _Chat() {
11671167
...(await new Promise<string[]>((res, rej) => {
11681168
setUploading(true);
11691169
const imagesData: string[] = [];
1170-
compressImage(file, 256 * 1024)
1170+
uploadImageRemote(file)
11711171
.then((dataUrl) => {
11721172
imagesData.push(dataUrl);
11731173
setUploading(false);
@@ -1209,7 +1209,7 @@ function _Chat() {
12091209
const imagesData: string[] = [];
12101210
for (let i = 0; i < files.length; i++) {
12111211
const file = event.target.files[i];
1212-
compressImage(file, 256 * 1024)
1212+
uploadImageRemote(file)
12131213
.then((dataUrl) => {
12141214
imagesData.push(dataUrl);
12151215
if (

app/constant.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
2121

2222
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
2323

24+
export const CACHE_URL_PREFIX = "/api/cache";
25+
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
26+
2427
export enum Path {
2528
Home = "/",
2629
Chat = "/chat",
@@ -239,7 +242,7 @@ const baiduModels = [
239242
"ernie-speed-128k",
240243
"ernie-speed-8k",
241244
"ernie-lite-8k",
242-
"ernie-tiny-8k"
245+
"ernie-tiny-8k",
243246
];
244247

245248
const bytedanceModels = [

app/utils/chat.ts

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import heic2any from "heic2any";
1+
import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant";
2+
import { RequestMessage } from "@/app/client/api";
23

3-
export function compressImage(file: File, maxSize: number): Promise<string> {
4+
export function compressImage(file: Blob, maxSize: number): Promise<string> {
45
return new Promise((resolve, reject) => {
56
const reader = new FileReader();
67
reader.onload = (readerEvent: any) => {
@@ -40,15 +41,104 @@ export function compressImage(file: File, maxSize: number): Promise<string> {
4041
reader.onerror = reject;
4142

4243
if (file.type.includes("heic")) {
43-
heic2any({ blob: file, toType: "image/jpeg" })
44-
.then((blob) => {
45-
reader.readAsDataURL(blob as Blob);
46-
})
47-
.catch((e) => {
48-
reject(e);
49-
});
44+
try {
45+
const heic2any = require("heic2any");
46+
heic2any({ blob: file, toType: "image/jpeg" })
47+
.then((blob: Blob) => {
48+
reader.readAsDataURL(blob);
49+
})
50+
.catch((e: any) => {
51+
reject(e);
52+
});
53+
} catch (e) {
54+
reject(e);
55+
}
5056
}
5157

5258
reader.readAsDataURL(file);
5359
});
5460
}
61+
62+
export async function preProcessImageContent(
63+
content: RequestMessage["content"],
64+
) {
65+
if (typeof content === "string") {
66+
return content;
67+
}
68+
const result = [];
69+
for (const part of content) {
70+
if (part?.type == "image_url" && part?.image_url?.url) {
71+
try {
72+
const url = await cacheImageToBase64Image(part?.image_url?.url);
73+
result.push({ type: part.type, image_url: { url } });
74+
} catch (error) {
75+
console.error("Error processing image URL:", error);
76+
}
77+
} else {
78+
result.push({ ...part });
79+
}
80+
}
81+
return result;
82+
}
83+
84+
const imageCaches: Record<string, string> = {};
85+
export function cacheImageToBase64Image(imageUrl: string) {
86+
if (imageUrl.includes(CACHE_URL_PREFIX)) {
87+
if (!imageCaches[imageUrl]) {
88+
const reader = new FileReader();
89+
return fetch(imageUrl, {
90+
method: "GET",
91+
mode: "cors",
92+
credentials: "include",
93+
})
94+
.then((res) => res.blob())
95+
.then(
96+
async (blob) =>
97+
(imageCaches[imageUrl] = await compressImage(blob, 256 * 1024)),
98+
); // compressImage
99+
}
100+
return Promise.resolve(imageCaches[imageUrl]);
101+
}
102+
return Promise.resolve(imageUrl);
103+
}
104+
105+
export function base64Image2Blob(base64Data: string, contentType: string) {
106+
const byteCharacters = atob(base64Data);
107+
const byteNumbers = new Array(byteCharacters.length);
108+
for (let i = 0; i < byteCharacters.length; i++) {
109+
byteNumbers[i] = byteCharacters.charCodeAt(i);
110+
}
111+
const byteArray = new Uint8Array(byteNumbers);
112+
return new Blob([byteArray], { type: contentType });
113+
}
114+
115+
export function uploadImage(file: File): Promise<string> {
116+
if (!window._SW_ENABLED) {
117+
// if serviceWorker register error, using compressImage
118+
return compressImage(file, 256 * 1024);
119+
}
120+
const body = new FormData();
121+
body.append("file", file);
122+
return fetch(UPLOAD_URL, {
123+
method: "post",
124+
body,
125+
mode: "cors",
126+
credentials: "include",
127+
})
128+
.then((res) => res.json())
129+
.then((res) => {
130+
console.log("res", res);
131+
if (res?.code == 0 && res?.data) {
132+
return res?.data;
133+
}
134+
throw Error(`upload Error: ${res?.msg}`);
135+
});
136+
}
137+
138+
export function removeImage(imageUrl: string) {
139+
return fetch(imageUrl, {
140+
method: "DELETE",
141+
mode: "cors",
142+
credentials: "include",
143+
});
144+
}

public/serviceWorker.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,59 @@
11
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
2+
const CHATGPT_NEXT_WEB_FILE_CACHE = "chatgpt-next-web-file";
3+
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};
24

35
self.addEventListener("activate", function (event) {
46
console.log("ServiceWorker activated.");
57
});
68

79
self.addEventListener("install", function (event) {
10+
self.skipWaiting(); // enable new version
811
event.waitUntil(
912
caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
1013
return cache.addAll([]);
1114
}),
1215
);
1316
});
1417

15-
self.addEventListener("fetch", (e) => {});
18+
async function upload(request, url) {
19+
const formData = await request.formData()
20+
const file = formData.getAll('file')[0]
21+
let ext = file.name.split('.').pop()
22+
if (ext === 'blob') {
23+
ext = file.type.split('/').pop()
24+
}
25+
const fileUrl = `${url.origin}/api/cache/${nanoid()}.${ext}`
26+
// console.debug('file', file, fileUrl, request)
27+
const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
28+
await cache.put(new Request(fileUrl), new Response(file, {
29+
headers: {
30+
'content-type': file.type,
31+
'content-length': file.size,
32+
'cache-control': 'no-cache', // file already store in disk
33+
'server': 'ServiceWorker',
34+
}
35+
}))
36+
return Response.json({ code: 0, data: fileUrl })
37+
}
38+
39+
async function remove(request, url) {
40+
const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
41+
const res = await cache.delete(request.url)
42+
return Response.json({ code: 0 })
43+
}
44+
45+
self.addEventListener("fetch", (e) => {
46+
const url = new URL(e.request.url);
47+
if (/^\/api\/cache/.test(url.pathname)) {
48+
if ('GET' == e.request.method) {
49+
e.respondWith(caches.match(e.request))
50+
}
51+
if ('POST' == e.request.method) {
52+
e.respondWith(upload(e.request, url))
53+
}
54+
if ('DELETE' == e.request.method) {
55+
e.respondWith(remove(e.request, url))
56+
}
57+
}
58+
});
59+

public/serviceWorkerRegister.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
if ('serviceWorker' in navigator) {
2-
window.addEventListener('load', function () {
2+
window.addEventListener('DOMContentLoaded', function () {
33
navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) {
44
console.log('ServiceWorker registration successful with scope: ', registration.scope);
5+
const sw = registration.installing || registration.waiting
6+
if (sw) {
7+
sw.onstatechange = function() {
8+
if (sw.state === 'installed') {
9+
// SW installed. Reload for SW intercept serving SW-enabled page.
10+
console.log('ServiceWorker installed reload page');
11+
window.location.reload();
12+
}
13+
}
14+
}
15+
registration.update().then(res => {
16+
console.log('ServiceWorker registration update: ', res);
17+
});
18+
window._SW_ENABLED = true
519
}, function (err) {
620
console.error('ServiceWorker registration failed: ', err);
721
});
22+
navigator.serviceWorker.addEventListener('controllerchange', function() {
23+
console.log('ServiceWorker controllerchange ');
24+
window.location.reload(true);
25+
});
826
});
9-
}
27+
}

0 commit comments

Comments
 (0)