feat: images using object service storage

This commit is contained in:
Hk-Gosuto 2023-12-10 21:23:40 +08:00
parent b84da5e120
commit 600a7d2197
6 changed files with 142 additions and 28 deletions

View 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";

View File

@ -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> = {

View File

@ -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) {

View 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;
}
}

View File

@ -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"

View File

@ -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"