Explorar o código

Merge branch 'main' into main

Kivi1998 hai 1 ano
pai
achega
5bf402710f

+ 1 - 1
README.md

@@ -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
 
-[![Deploy with Vercel](https://vercel.com/button)](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)
+[![Deploy with Vercel](https://vercel.com/button)](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)
 
 [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA)
 

+ 0 - 43
app/api/cors/[...path]/route.ts

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

+ 73 - 0
app/api/upstash/[action]/[...key]/route.ts

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

+ 112 - 0
app/api/webdav/[...path]/route.ts

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

+ 2 - 0
app/components/chat.tsx

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

+ 1 - 1
app/constant.ts

@@ -23,7 +23,7 @@ export enum Path {
 }
 
 export enum ApiPath {
-  Cors = "/api/cors",
+  Cors = "",
   OpenAI = "/api/openai",
 }
 

+ 10 - 1
app/store/sync.ts

@@ -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;
     },
   },

+ 7 - 5
app/utils.ts

@@ -292,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") ||
-    model.includes("vision")
-  );
+  // 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));
 }

+ 20 - 14
app/utils/cloud/upstash.ts

@@ -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;
     },
   };
 }

+ 20 - 14
app/utils/cloud/webdav.ts

@@ -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;
     },
   };
 }

+ 3 - 34
app/utils/cors.ts

@@ -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);
-}

+ 1 - 1
src-tauri/tauri.conf.json

@@ -9,7 +9,7 @@
   },
   "package": {
     "productName": "NextChat",
-    "version": "2.11.2"
+    "version": "2.11.3"
   },
   "tauri": {
     "allowlist": {