mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-24 06:30:16 +09:00
feat: images using object service storage
This commit is contained in:
parent
b84da5e120
commit
600a7d2197
57
app/api/file/upload/route.ts
Normal file
57
app/api/file/upload/route.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "../../auth";
|
||||||
|
import S3FileStorage from "../../../utils/s3_file_storage";
|
||||||
|
|
||||||
|
async function handle(req: NextRequest) {
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = await req.formData();
|
||||||
|
const image = formData.get("file") as File;
|
||||||
|
|
||||||
|
const imageReader = image.stream().getReader();
|
||||||
|
const imageData: number[] = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await imageReader.read();
|
||||||
|
if (done) break;
|
||||||
|
imageData.push(...value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.from(imageData);
|
||||||
|
|
||||||
|
var fileName = `${Date.now()}.png`;
|
||||||
|
await S3FileStorage.put(fileName, buffer);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
fileName: fileName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
msg: (e as Error).message,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
@ -2,6 +2,7 @@ import { getClientConfig } from "../config/client";
|
|||||||
import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant";
|
import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant";
|
||||||
import { ChatMessage, ModelType, useAccessStore } from "../store";
|
import { ChatMessage, ModelType, useAccessStore } from "../store";
|
||||||
import { ChatGPTApi } from "./platforms/openai";
|
import { ChatGPTApi } from "./platforms/openai";
|
||||||
|
import { FileApi } from "./platforms/utils";
|
||||||
|
|
||||||
export const ROLES = ["system", "user", "assistant"] as const;
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
export type MessageRole = (typeof ROLES)[number];
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
@ -97,9 +98,11 @@ export abstract class ToolApi {
|
|||||||
|
|
||||||
export class ClientApi {
|
export class ClientApi {
|
||||||
public llm: LLMApi;
|
public llm: LLMApi;
|
||||||
|
public file: FileApi;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.llm = new ChatGPTApi();
|
this.llm = new ChatGPTApi();
|
||||||
|
this.file = new FileApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
config() {}
|
config() {}
|
||||||
@ -150,6 +153,32 @@ export class ClientApi {
|
|||||||
|
|
||||||
export const api = new ClientApi();
|
export const api = new ClientApi();
|
||||||
|
|
||||||
|
export function getAuthHeaders() {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
||||||
|
const authHeader = isAzure ? "api-key" : "Authorization";
|
||||||
|
const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey;
|
||||||
|
|
||||||
|
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
|
||||||
|
const validString = (x: string) => x && x.length > 0;
|
||||||
|
|
||||||
|
// use user's api key first
|
||||||
|
if (validString(apiKey)) {
|
||||||
|
headers[authHeader] = makeBearer(apiKey);
|
||||||
|
} else if (
|
||||||
|
accessStore.enabledAccessControl() &&
|
||||||
|
validString(accessStore.accessCode)
|
||||||
|
) {
|
||||||
|
headers[authHeader] = makeBearer(
|
||||||
|
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
export function getHeaders() {
|
export function getHeaders() {
|
||||||
const accessStore = useAccessStore.getState();
|
const accessStore = useAccessStore.getState();
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
|
@ -74,7 +74,8 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async chat(options: ChatOptions) {
|
async chat(options: ChatOptions) {
|
||||||
const messages = options.messages.map((v) => {
|
const messages: any[] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
let message: {
|
let message: {
|
||||||
role: string;
|
role: string;
|
||||||
content: { type: string; text?: string; image_url?: { url: string } }[];
|
content: { type: string; text?: string; image_url?: { url: string } }[];
|
||||||
@ -87,15 +88,25 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
text: v.content,
|
text: v.content,
|
||||||
});
|
});
|
||||||
if (v.image_url) {
|
if (v.image_url) {
|
||||||
|
await fetch(v.image_url)
|
||||||
|
.then((response) => response.arrayBuffer())
|
||||||
|
.then((buffer) => {
|
||||||
|
const base64Data = btoa(
|
||||||
|
String.fromCharCode(...new Uint8Array(buffer)),
|
||||||
|
);
|
||||||
message.content.push({
|
message.content.push({
|
||||||
type: "image_url",
|
type: "image_url",
|
||||||
image_url: {
|
image_url: {
|
||||||
url: v.image_url,
|
url: `data:image/jpeg;base64,${base64Data}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
return message;
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
const modelConfig = {
|
const modelConfig = {
|
||||||
...useAppConfig.getState().modelConfig,
|
...useAppConfig.getState().modelConfig,
|
||||||
@ -104,7 +115,6 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
model: options.config.model,
|
model: options.config.model,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
messages,
|
messages,
|
||||||
stream: options.config.stream,
|
stream: options.config.stream,
|
||||||
@ -177,7 +187,6 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
controller.signal.onabort = finish;
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
|
19
app/client/platforms/utils.ts
Normal file
19
app/client/platforms/utils.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { getAuthHeaders } from "../api";
|
||||||
|
|
||||||
|
export class FileApi {
|
||||||
|
async upload(file: any): Promise<void> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
var headers = getAuthHeaders();
|
||||||
|
var res = await fetch("/api/file/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const resJson = await res.json();
|
||||||
|
console.log(resJson);
|
||||||
|
return resJson.fileName;
|
||||||
|
}
|
||||||
|
}
|
@ -96,6 +96,7 @@ import { ExportMessageModal } from "./exporter";
|
|||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { useAllModels } from "../utils/hooks";
|
import { useAllModels } from "../utils/hooks";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { api } from "../client/api";
|
||||||
|
|
||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
loading: () => <LoadingIcon />,
|
loading: () => <LoadingIcon />,
|
||||||
@ -457,18 +458,13 @@ export function ChatActions(props: {
|
|||||||
document.getElementById("chat-image-file-select-upload")?.click();
|
document.getElementById("chat-image-file-select-upload")?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
const onImageSelected = (e: any) => {
|
const onImageSelected = async (e: any) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
const filename = file.name;
|
const fileName = await api.file.upload(file);
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => {
|
|
||||||
const base64 = reader.result;
|
|
||||||
props.imageSelected({
|
props.imageSelected({
|
||||||
filename,
|
fileName,
|
||||||
base64,
|
fileUrl: `/api/file/${fileName}`,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
e.target.value = null;
|
e.target.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -783,7 +779,7 @@ function _Chat() {
|
|||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
chatStore
|
chatStore
|
||||||
.onUserInput(userInput, userImage?.base64)
|
.onUserInput(userInput, userImage?.fileUrl)
|
||||||
.then(() => setIsLoading(false));
|
.then(() => setIsLoading(false));
|
||||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
||||||
localStorage.setItem(LAST_INPUT_IMAGE_KEY, userImage);
|
localStorage.setItem(LAST_INPUT_IMAGE_KEY, userImage);
|
||||||
@ -935,7 +931,9 @@ function _Chat() {
|
|||||||
|
|
||||||
// resend the message
|
// resend the message
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
chatStore.onUserInput(userMessage.content).then(() => setIsLoading(false));
|
chatStore
|
||||||
|
.onUserInput(userMessage.content, userMessage.image_url)
|
||||||
|
.then(() => setIsLoading(false));
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -992,7 +990,7 @@ function _Chat() {
|
|||||||
...createMessage({
|
...createMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: userInput,
|
content: userInput,
|
||||||
image_url: userImage?.base64,
|
image_url: userImage?.fileUrl,
|
||||||
}),
|
}),
|
||||||
preview: true,
|
preview: true,
|
||||||
},
|
},
|
||||||
@ -1005,7 +1003,7 @@ function _Chat() {
|
|||||||
isLoading,
|
isLoading,
|
||||||
session.messages,
|
session.messages,
|
||||||
userInput,
|
userInput,
|
||||||
userImage?.base64,
|
userImage?.fileUrl,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [msgRenderIndex, _setMsgRenderIndex] = useState(
|
const [msgRenderIndex, _setMsgRenderIndex] = useState(
|
||||||
@ -1427,7 +1425,7 @@ function _Chat() {
|
|||||||
style={{ position: "relative", width: "48px", height: "48px" }}
|
style={{ position: "relative", width: "48px", height: "48px" }}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={userImage.base64}
|
src={userImage.fileUrl}
|
||||||
alt={userImage.filename}
|
alt={userImage.filename}
|
||||||
title={userImage.filename}
|
title={userImage.filename}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
|
|
||||||
这边以 `又拍云` 做为演示,其它运营商请查询对应文档。
|
这边以 `又拍云` 做为演示,其它运营商请查询对应文档。
|
||||||
|
|
||||||
|
参考: https://help.upyun.com/knowledge-base/aws-s3%E5%85%BC%E5%AE%B9/#e585bce5aeb9e5b7a5e585b7e7a4bae4be8b
|
||||||
|
|
||||||
1. 登录 [又拍云 - 加速在线业务 - CDN加速 - 云存储 (upyun.com)](https://www.upyun.com/)
|
1. 登录 [又拍云 - 加速在线业务 - CDN加速 - 云存储 (upyun.com)](https://www.upyun.com/)
|
||||||
2. 注册账户
|
2. 注册账户
|
||||||
3. 进入”云存储“控制台[又拍云控制台 (upyun.com)](https://console.upyun.com/services/file/)
|
3. 进入"云存储"控制台[又拍云控制台 (upyun.com)](https://console.upyun.com/services/file/)
|
||||||
4. 创建一个服务,记录你的服务名
|
4. 创建一个服务,记录你的服务名
|
||||||
5. 进入"用户管理","操作员"创建一个"操作员"并赋予相应权限
|
5. 进入"用户管理","操作员"创建一个"操作员"并赋予相应权限
|
||||||
6. 编辑"操作员"复制"AccessKey"和"SecretAccessKey"
|
6. 编辑"操作员"复制"AccessKey"和"SecretAccessKey"
|
||||||
|
Loading…
Reference in New Issue
Block a user