Merge branch 'main' into feat/voice-input
This commit is contained in:
commit
52316785d1
|
@ -25,7 +25,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat)
|
||||
|
||||
[](https://zeabur.com/templates/ZBUEFA)
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const [protocol, ...subpath] = params.path;
|
||||
const targetUrl = `${protocol}://${subpath.join("/")}`;
|
||||
|
||||
const method = req.headers.get("method") ?? undefined;
|
||||
const shouldNotHaveBody = ["get", "head"].includes(
|
||||
method?.toLowerCase() ?? "",
|
||||
);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
authorization: req.headers.get("authorization") ?? "",
|
||||
},
|
||||
body: shouldNotHaveBody ? null : req.body,
|
||||
method,
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
};
|
||||
|
||||
const fetchResult = await fetch(targetUrl, fetchOptions);
|
||||
|
||||
console.log("[Any Proxy]", targetUrl, {
|
||||
status: fetchResult.status,
|
||||
statusText: fetchResult.statusText,
|
||||
});
|
||||
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const GET = handle;
|
||||
export const OPTIONS = handle;
|
||||
|
||||
export const runtime = "edge";
|
|
@ -0,0 +1,73 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { action: string; key: string[] } },
|
||||
) {
|
||||
const requestUrl = new URL(req.url);
|
||||
const endpoint = requestUrl.searchParams.get("endpoint");
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
const [...key] = params.key;
|
||||
// only allow to request to *.upstash.io
|
||||
if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.key.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// only allow upstash get and set method
|
||||
if (params.action !== "get" && params.action !== "set") {
|
||||
console.log("[Upstash Route] forbidden action ", params.action);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.action,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;
|
||||
|
||||
const method = req.method;
|
||||
const shouldNotHaveBody = ["get", "head"].includes(
|
||||
method?.toLowerCase() ?? "",
|
||||
);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
authorization: req.headers.get("authorization") ?? "",
|
||||
},
|
||||
body: shouldNotHaveBody ? null : req.body,
|
||||
method,
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
};
|
||||
|
||||
console.log("[Upstash Proxy]", targetUrl, fetchOptions);
|
||||
const fetchResult = await fetch(targetUrl, fetchOptions);
|
||||
|
||||
console.log("[Any Proxy]", targetUrl, {
|
||||
status: fetchResult.status,
|
||||
statusText: fetchResult.statusText,
|
||||
});
|
||||
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const GET = handle;
|
||||
export const OPTIONS = handle;
|
||||
|
||||
export const runtime = "edge";
|
|
@ -0,0 +1,112 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { STORAGE_KEY } from "../../../constant";
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
const folder = STORAGE_KEY;
|
||||
const fileName = `${folder}/backup.json`;
|
||||
|
||||
const requestUrl = new URL(req.url);
|
||||
let endpoint = requestUrl.searchParams.get("endpoint");
|
||||
if (!endpoint?.endsWith("/")) {
|
||||
endpoint += "/";
|
||||
}
|
||||
const endpointPath = params.path.join("/");
|
||||
|
||||
// only allow MKCOL, GET, PUT
|
||||
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// for MKCOL request, only allow request ${folder}
|
||||
if (
|
||||
req.method == "MKCOL" &&
|
||||
!new URL(endpointPath).pathname.endsWith(folder)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// for GET request, only allow request ending with fileName
|
||||
if (
|
||||
req.method == "GET" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// for PUT request, only allow request ending with fileName
|
||||
if (
|
||||
req.method == "PUT" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = `${endpoint + endpointPath}`;
|
||||
|
||||
const method = req.method;
|
||||
const shouldNotHaveBody = ["get", "head"].includes(
|
||||
method?.toLowerCase() ?? "",
|
||||
);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
authorization: req.headers.get("authorization") ?? "",
|
||||
},
|
||||
body: shouldNotHaveBody ? null : req.body,
|
||||
method,
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
};
|
||||
|
||||
const fetchResult = await fetch(targetUrl, fetchOptions);
|
||||
|
||||
console.log("[Any Proxy]", targetUrl, {
|
||||
status: fetchResult.status,
|
||||
statusText: fetchResult.statusText,
|
||||
});
|
||||
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const GET = handle;
|
||||
export const OPTIONS = handle;
|
||||
|
||||
export const runtime = "edge";
|
|
@ -116,7 +116,7 @@ export class ChatGPTApi implements LLMApi {
|
|||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: Math.max(modelConfig.max_tokens, 4096),
|
||||
value: modelConfig.max_tokens,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -219,6 +219,8 @@ function useSubmitHandler() {
|
|||
}, []);
|
||||
|
||||
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// Fix Chinese input method "Enter" on Safari
|
||||
if (e.keyCode == 229) return false;
|
||||
if (e.key !== "Enter") return false;
|
||||
if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current))
|
||||
return false;
|
||||
|
@ -1098,6 +1100,47 @@ function _Chat() {
|
|||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
if(!isVisionModel(currentModel)){return;}
|
||||
const items = (event.clipboardData || window.clipboardData).items;
|
||||
for (const item of items) {
|
||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
||||
event.preventDefault();
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
const images: string[] = [];
|
||||
images.push(...attachImages);
|
||||
images.push(
|
||||
...(await new Promise<string[]>((res, rej) => {
|
||||
setUploading(true);
|
||||
const imagesData: string[] = [];
|
||||
compressImage(file, 256 * 1024)
|
||||
.then((dataUrl) => {
|
||||
imagesData.push(dataUrl);
|
||||
setUploading(false);
|
||||
res(imagesData);
|
||||
})
|
||||
.catch((e) => {
|
||||
setUploading(false);
|
||||
rej(e);
|
||||
});
|
||||
})),
|
||||
);
|
||||
const imagesLength = images.length;
|
||||
|
||||
if (imagesLength > 3) {
|
||||
images.splice(3, imagesLength - 3);
|
||||
}
|
||||
setAttachImages(images);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[attachImages, chatStore],
|
||||
);
|
||||
|
||||
async function uploadImage() {
|
||||
const images: string[] = [];
|
||||
|
@ -1448,6 +1491,7 @@ function _Chat() {
|
|||
onKeyDown={onInputKeyDown}
|
||||
onFocus={scrollToBottom}
|
||||
onClick={scrollToBottom}
|
||||
onPaste={handlePaste}
|
||||
rows={inputRows}
|
||||
autoFocus={autoFocus}
|
||||
style={{
|
||||
|
|
|
@ -21,6 +21,7 @@ export function AvatarPicker(props: {
|
|||
}) {
|
||||
return (
|
||||
<EmojiPicker
|
||||
width={"100%"}
|
||||
lazyLoadEmojis
|
||||
theme={EmojiTheme.AUTO}
|
||||
getEmojiUrl={getEmojiUrl}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.edit-prompt-modal {
|
||||
|
|
|
@ -693,7 +693,9 @@ export function Settings() {
|
|||
>
|
||||
<div
|
||||
className={styles.avatar}
|
||||
onClick={() => setShowEmojiPicker(true)}
|
||||
onClick={() => {
|
||||
setShowEmojiPicker(!showEmojiPicker);
|
||||
}}
|
||||
>
|
||||
<Avatar avatar={config.avatar} />
|
||||
</div>
|
||||
|
|
|
@ -14,17 +14,24 @@
|
|||
|
||||
.popover-content {
|
||||
position: absolute;
|
||||
width: 350px;
|
||||
animation: slide-in 0.3s ease;
|
||||
right: 0;
|
||||
top: calc(100% + 10px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.popover-content {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
.popover-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.list-item {
|
||||
|
|
|
@ -26,10 +26,10 @@ export function Popover(props: {
|
|||
<div className={styles.popover}>
|
||||
{props.children}
|
||||
{props.open && (
|
||||
<div className={styles["popover-content"]}>
|
||||
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
|
||||
{props.content}
|
||||
</div>
|
||||
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
|
||||
)}
|
||||
{props.open && (
|
||||
<div className={styles["popover-content"]}>{props.content}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -31,6 +31,7 @@ declare global {
|
|||
GOOGLE_API_KEY?: string;
|
||||
GOOGLE_URL?: string;
|
||||
|
||||
// google tag manager
|
||||
GTM_ID?: string;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export enum Path {
|
|||
}
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "/api/cors",
|
||||
Cors = "",
|
||||
OpenAI = "/api/openai",
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ declare interface Window {
|
|||
};
|
||||
fs: {
|
||||
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
|
||||
writeTextFile(path: string, data: string): Promise<void>;
|
||||
};
|
||||
notification:{
|
||||
requestPermission(): Promise<Permission>;
|
||||
|
|
|
@ -7,8 +7,8 @@ const tw = {
|
|||
WIP: "該功能仍在開發中……",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "檢測到無效 API Key,請前往[設定](/#/settings)頁檢查 API Key 是否配置正確。"
|
||||
: "訪問密碼不正確或為空,請前往[登錄](/#/auth)頁輸入正確的訪問密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。",
|
||||
? "檢測到無效 API Key,請前往[設定](/#/settings)頁檢查 API Key 是否設定正確。"
|
||||
: "訪問密碼不正確或為空,請前往[登入](/#/auth)頁輸入正確的訪問密碼,或者在[設定](/#/settings)頁填入你自己的 OpenAI API Key。",
|
||||
},
|
||||
|
||||
Auth: {
|
||||
|
@ -17,7 +17,7 @@ const tw = {
|
|||
SubTips: "或者輸入你的 OpenAI 或 Google API 密鑰",
|
||||
Input: "在此處填寫訪問碼",
|
||||
Confirm: "確認",
|
||||
Later: "稍後再說",
|
||||
Later: "稍候再說",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 則對話`,
|
||||
|
@ -53,8 +53,8 @@ const tw = {
|
|||
del: "刪除聊天",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "停止響應",
|
||||
ToBottom: "滾到最新",
|
||||
Stop: "停止回應",
|
||||
ToBottom: "移至最新",
|
||||
Theme: {
|
||||
auto: "自動主題",
|
||||
light: "亮色模式",
|
||||
|
@ -107,7 +107,7 @@ const tw = {
|
|||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "搜索消息",
|
||||
Search: "查詢消息",
|
||||
All: "選取全部",
|
||||
Latest: "最近幾條",
|
||||
Clear: "清除選中",
|
||||
|
@ -133,15 +133,15 @@ const tw = {
|
|||
Danger: {
|
||||
Reset: {
|
||||
Title: "重置所有設定",
|
||||
SubTitle: "重置所有設定項回默認值",
|
||||
SubTitle: "重置所有設定項回預設值",
|
||||
Action: "立即重置",
|
||||
Confirm: "確認重置所有設定?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "清除所有數據",
|
||||
SubTitle: "清除所有聊天、設定數據",
|
||||
Title: "清除所有資料",
|
||||
SubTitle: "清除所有聊天、設定資料",
|
||||
Action: "立即清除",
|
||||
Confirm: "確認清除所有聊天、設定數據?",
|
||||
Confirm: "確認清除所有聊天、設定資料?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
|
@ -182,14 +182,14 @@ const tw = {
|
|||
SubTitle: "根據對話內容生成合適的標題",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "雲端數據",
|
||||
CloudState: "雲端資料",
|
||||
NotSyncYet: "還沒有進行過同步",
|
||||
Success: "同步成功",
|
||||
Fail: "同步失敗",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "配置雲端同步",
|
||||
Title: "設定雲端同步",
|
||||
Check: "檢查可用性",
|
||||
},
|
||||
SyncType: {
|
||||
|
@ -218,7 +218,7 @@ const tw = {
|
|||
},
|
||||
},
|
||||
|
||||
LocalState: "本地數據",
|
||||
LocalState: "本地資料",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} 次對話,${overview.message} 條消息,${overview.prompt} 條提示詞,${overview.mask} 個面具`;
|
||||
},
|
||||
|
@ -385,7 +385,7 @@ const tw = {
|
|||
Edit: "前置上下文和歷史記憶",
|
||||
Add: "新增一條",
|
||||
Clear: "上下文已清除",
|
||||
Revert: "恢覆上下文",
|
||||
Revert: "恢復上下文",
|
||||
},
|
||||
Plugin: { Name: "外掛" },
|
||||
FineTuned: { Sysmessage: "你是一個助手" },
|
||||
|
@ -440,8 +440,8 @@ const tw = {
|
|||
More: "搜尋更多",
|
||||
},
|
||||
URLCommand: {
|
||||
Code: "檢測到鏈接中已經包含訪問碼,是否自動填入?",
|
||||
Settings: "檢測到鏈接中包含了預制設置,是否自動填入?",
|
||||
Code: "檢測到連結中已經包含訪問碼,是否自動填入?",
|
||||
Settings: "檢測到連結中包含了預設設定,是否自動填入?",
|
||||
},
|
||||
UI: {
|
||||
Confirm: "確認",
|
||||
|
@ -452,7 +452,7 @@ const tw = {
|
|||
Export: "導出",
|
||||
Import: "導入",
|
||||
Sync: "同步",
|
||||
Config: "配置",
|
||||
Config: "設定",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
|
|
|
@ -118,7 +118,7 @@ export const useSyncStore = createPersistStore(
|
|||
}),
|
||||
{
|
||||
name: StoreKey.Sync,
|
||||
version: 1.1,
|
||||
version: 1.2,
|
||||
|
||||
migrate(persistedState, version) {
|
||||
const newState = persistedState as typeof DEFAULT_SYNC_STATE;
|
||||
|
@ -127,6 +127,15 @@ export const useSyncStore = createPersistStore(
|
|||
newState.upstash.username = STORAGE_KEY;
|
||||
}
|
||||
|
||||
if (version < 1.2) {
|
||||
if (
|
||||
(persistedState as typeof DEFAULT_SYNC_STATE).proxyUrl ===
|
||||
"/api/cors/"
|
||||
) {
|
||||
newState.proxyUrl = "";
|
||||
}
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
},
|
||||
|
|
21
app/utils.ts
21
app/utils.ts
|
@ -9,8 +9,9 @@ export function trimTopic(topic: string) {
|
|||
// This will remove the specified punctuation from the end of the string
|
||||
// and also trim quotes from both the start and end if they exist.
|
||||
return topic
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
.replace(/[,。!?”“"、,.!?]*$/, "");
|
||||
// fix for gemini
|
||||
.replace(/^["“”*]+|["“”*]+$/g, "")
|
||||
.replace(/[,。!?”“"、,.!?*]*$/, "");
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string) {
|
||||
|
@ -56,9 +57,9 @@ export async function downloadAs(text: string, filename: string) {
|
|||
|
||||
if (result !== null) {
|
||||
try {
|
||||
await window.__TAURI__.fs.writeBinaryFile(
|
||||
await window.__TAURI__.fs.writeTextFile(
|
||||
result,
|
||||
new Uint8Array([...text].map((c) => c.charCodeAt(0))),
|
||||
text
|
||||
);
|
||||
showToast(Locale.Download.Success);
|
||||
} catch (error) {
|
||||
|
@ -291,9 +292,11 @@ export function getMessageImages(message: RequestMessage): string[] {
|
|||
}
|
||||
|
||||
export function isVisionModel(model: string) {
|
||||
return (
|
||||
model.startsWith("gpt-4-vision") ||
|
||||
model.startsWith("gemini-pro-vision") ||
|
||||
!DEFAULT_MODELS.find((m) => m.name == model)
|
||||
);
|
||||
// Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using)
|
||||
const visionKeywords = [
|
||||
"vision",
|
||||
"claude-3",
|
||||
];
|
||||
|
||||
return visionKeywords.some(keyword => model.includes(keyword));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { STORAGE_KEY } from "@/app/constant";
|
||||
import { SyncStore } from "@/app/store/sync";
|
||||
import { corsFetch } from "../cors";
|
||||
import { chunks } from "../format";
|
||||
|
||||
export type UpstashConfig = SyncStore["upstash"];
|
||||
|
@ -18,10 +17,9 @@ export function createUpstashClient(store: SyncStore) {
|
|||
return {
|
||||
async check() {
|
||||
try {
|
||||
const res = await corsFetch(this.path(`get/${storeKey}`), {
|
||||
const res = await fetch(this.path(`get/${storeKey}`, proxyUrl), {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
console.log("[Upstash] check", res.status, res.statusText);
|
||||
return [200].includes(res.status);
|
||||
|
@ -32,10 +30,9 @@ export function createUpstashClient(store: SyncStore) {
|
|||
},
|
||||
|
||||
async redisGet(key: string) {
|
||||
const res = await corsFetch(this.path(`get/${key}`), {
|
||||
const res = await fetch(this.path(`get/${key}`, proxyUrl), {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[Upstash] get key = ", key, res.status, res.statusText);
|
||||
|
@ -45,11 +42,10 @@ export function createUpstashClient(store: SyncStore) {
|
|||
},
|
||||
|
||||
async redisSet(key: string, value: string) {
|
||||
const res = await corsFetch(this.path(`set/${key}`), {
|
||||
const res = await fetch(this.path(`set/${key}`, proxyUrl), {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: value,
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[Upstash] set key = ", key, res.status, res.statusText);
|
||||
|
@ -84,18 +80,28 @@ export function createUpstashClient(store: SyncStore) {
|
|||
Authorization: `Bearer ${config.apiKey}`,
|
||||
};
|
||||
},
|
||||
path(path: string) {
|
||||
let url = config.endpoint;
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
path(path: string, proxyUrl: string = "") {
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) {
|
||||
proxyUrl += "/";
|
||||
}
|
||||
|
||||
let url;
|
||||
if (proxyUrl.length > 0 || proxyUrl === "/") {
|
||||
let u = new URL(proxyUrl + "/api/upstash/" + path);
|
||||
// add query params
|
||||
u.searchParams.append("endpoint", config.endpoint);
|
||||
url = u.toString();
|
||||
} else {
|
||||
url = "/api/upstash/" + path + "?endpoint=" + config.endpoint;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { STORAGE_KEY } from "@/app/constant";
|
||||
import { SyncStore } from "@/app/store/sync";
|
||||
import { corsFetch } from "../cors";
|
||||
|
||||
export type WebDAVConfig = SyncStore["webdav"];
|
||||
export type WebDavClient = ReturnType<typeof createWebDavClient>;
|
||||
|
@ -15,10 +14,9 @@ export function createWebDavClient(store: SyncStore) {
|
|||
return {
|
||||
async check() {
|
||||
try {
|
||||
const res = await corsFetch(this.path(folder), {
|
||||
const res = await fetch(this.path(folder, proxyUrl), {
|
||||
method: "MKCOL",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
console.log("[WebDav] check", res.status, res.statusText);
|
||||
return [201, 200, 404, 301, 302, 307, 308].includes(res.status);
|
||||
|
@ -30,10 +28,9 @@ export function createWebDavClient(store: SyncStore) {
|
|||
},
|
||||
|
||||
async get(key: string) {
|
||||
const res = await corsFetch(this.path(fileName), {
|
||||
const res = await fetch(this.path(fileName, proxyUrl), {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] get key = ", key, res.status, res.statusText);
|
||||
|
@ -42,11 +39,10 @@ export function createWebDavClient(store: SyncStore) {
|
|||
},
|
||||
|
||||
async set(key: string, value: string) {
|
||||
const res = await corsFetch(this.path(fileName), {
|
||||
const res = await fetch(this.path(fileName, proxyUrl), {
|
||||
method: "PUT",
|
||||
headers: this.headers(),
|
||||
body: value,
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] set key = ", key, res.status, res.statusText);
|
||||
|
@ -59,18 +55,28 @@ export function createWebDavClient(store: SyncStore) {
|
|||
authorization: `Basic ${auth}`,
|
||||
};
|
||||
},
|
||||
path(path: string) {
|
||||
let url = config.endpoint;
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
path(path: string, proxyUrl: string = "") {
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) {
|
||||
proxyUrl += "/";
|
||||
}
|
||||
|
||||
let url;
|
||||
if (proxyUrl.length > 0 || proxyUrl === "/") {
|
||||
let u = new URL(proxyUrl + "/api/webdav/" + path);
|
||||
// add query params
|
||||
u.searchParams.append("endpoint", config.endpoint);
|
||||
url = u.toString();
|
||||
} else {
|
||||
url = "/api/upstash/" + path + "?endpoint=" + config.endpoint;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ import { ApiPath, DEFAULT_API_HOST } from "../constant";
|
|||
export function corsPath(path: string) {
|
||||
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : "";
|
||||
|
||||
if (baseUrl === "" && path === "") {
|
||||
return "";
|
||||
}
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
@ -14,37 +17,3 @@ export function corsPath(path: string) {
|
|||
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
|
||||
export function corsFetch(
|
||||
url: string,
|
||||
options: RequestInit & {
|
||||
proxyUrl?: string;
|
||||
},
|
||||
) {
|
||||
if (!url.startsWith("http")) {
|
||||
throw Error("[CORS Fetch] url must starts with http/https");
|
||||
}
|
||||
|
||||
let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors);
|
||||
if (!proxyUrl.endsWith("/")) {
|
||||
proxyUrl += "/";
|
||||
}
|
||||
|
||||
url = url.replace("://", "/");
|
||||
|
||||
const corsOptions = {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: options.method
|
||||
? {
|
||||
...options.headers,
|
||||
method: options.method,
|
||||
}
|
||||
: options.headers,
|
||||
};
|
||||
|
||||
const corsUrl = proxyUrl + url;
|
||||
console.info("[CORS] target = ", corsUrl);
|
||||
|
||||
return fetch(corsUrl, corsOptions);
|
||||
}
|
||||
|
|
|
@ -67,4 +67,5 @@
|
|||
"resolutions": {
|
||||
"lint-staged/yaml": "^2.2.2"
|
||||
}
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
"package": {
|
||||
"productName": "NextChat",
|
||||
"version": "2.11.2"
|
||||
"version": "2.11.3"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
|
Loading…
Reference in New Issue