Browse Source

feat: #1000 ready to support client-side only

Yidadaa 1 year ago
parent
commit
50cd33dbb2

+ 22 - 1
app/client/api.ts

@@ -1,5 +1,5 @@
 import { ACCESS_CODE_PREFIX } from "../constant";
-import { ChatMessage, ModelConfig, ModelType, useAccessStore } from "../store";
+import { ChatMessage, ModelType, useAccessStore } from "../store";
 import { ChatGPTApi } from "./platforms/openai";
 
 export const ROLES = ["system", "user", "assistant"] as const;
@@ -42,6 +42,27 @@ export abstract class LLMApi {
   abstract usage(): Promise<LLMUsage>;
 }
 
+type ProviderName = "openai" | "azure" | "claude" | "palm";
+
+interface Model {
+  name: string;
+  provider: ProviderName;
+  ctxlen: number;
+}
+
+interface ChatProvider {
+  name: ProviderName;
+  apiConfig: {
+    baseUrl: string;
+    apiKey: string;
+    summaryModel: Model;
+  };
+  models: Model[];
+
+  chat: () => void;
+  usage: () => void;
+}
+
 export class ClientApi {
   public llm: LLMApi;
 

+ 5 - 0
app/components/home.tsx

@@ -24,6 +24,7 @@ import {
 import { SideBar } from "./sidebar";
 import { useAppConfig } from "../store/config";
 import { AuthPage } from "./auth";
+import { getClientConfig } from "../config/client";
 
 export function Loading(props: { noLogo?: boolean }) {
   return (
@@ -147,6 +148,10 @@ function Screen() {
 export function Home() {
   useSwitchTheme();
 
+  useEffect(() => {
+    console.log("[Config] got config from build time", getClientConfig());
+  }, []);
+
   if (!useHasHydrated()) {
     return <Loading />;
   }

+ 17 - 1
app/components/settings.tsx

@@ -1,4 +1,4 @@
-import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
+import { useState, useEffect, useMemo } from "react";
 
 import styles from "./settings.module.scss";
 
@@ -45,6 +45,7 @@ import { ErrorBoundary } from "./error";
 import { InputRange } from "./input-range";
 import { useNavigate } from "react-router-dom";
 import { Avatar, AvatarPicker } from "./emoji";
+import { getClientConfig } from "../config/client";
 
 function EditPromptModal(props: { id: number; onClose: () => void }) {
   const promptStore = usePromptStore();
@@ -541,6 +542,21 @@ export function Settings() {
               />
             )}
           </ListItem>
+
+          {!accessStore.hideUserApiKey ? (
+            <ListItem
+              title={Locale.Settings.Endpoint.Title}
+              subTitle={Locale.Settings.Endpoint.SubTitle}
+            >
+              <input
+                type="text"
+                value={accessStore.openaiUrl}
+                onChange={(e) =>
+                  accessStore.updateOpenAiUrl(e.currentTarget.value)
+                }
+              ></input>
+            </ListItem>
+          ) : null}
         </List>
 
         <List>

+ 16 - 13
app/config/build.ts

@@ -1,16 +1,3 @@
-const COMMIT_ID: string = (() => {
-  try {
-    const childProcess = require("child_process");
-    return childProcess
-      .execSync('git log -1 --format="%at000" --date=unix')
-      .toString()
-      .trim();
-  } catch (e) {
-    console.error("[Build Config] No git or not from git repo.");
-    return "unknown";
-  }
-})();
-
 export const getBuildConfig = () => {
   if (typeof process === "undefined") {
     throw Error(
@@ -18,7 +5,23 @@ export const getBuildConfig = () => {
     );
   }
 
+  const COMMIT_ID: string = (() => {
+    try {
+      const childProcess = require("child_process");
+      return childProcess
+        .execSync('git log -1 --format="%at000" --date=unix')
+        .toString()
+        .trim();
+    } catch (e) {
+      console.error("[Build Config] No git or not from git repo.");
+      return "unknown";
+    }
+  })();
+
   return {
     commitId: COMMIT_ID,
+    buildMode: process.env.BUILD_MODE ?? "standalone",
   };
 };
+
+export type BuildConfig = ReturnType<typeof getBuildConfig>;

+ 27 - 0
app/config/client.ts

@@ -0,0 +1,27 @@
+import { BuildConfig, getBuildConfig } from "./build";
+
+export function getClientConfig() {
+  if (typeof document !== "undefined") {
+    // client side
+    return JSON.parse(queryMeta("config")) as BuildConfig;
+  }
+
+  if (typeof process !== "undefined") {
+    // server side
+    return getBuildConfig();
+  }
+}
+
+function queryMeta(key: string, defaultValue?: string): string {
+  let ret: string;
+  if (document) {
+    const meta = document.head.querySelector(
+      `meta[name='${key}']`,
+    ) as HTMLMetaElement;
+    ret = meta?.content ?? "";
+  } else {
+    ret = defaultValue ?? "";
+  }
+
+  return ret;
+}

+ 1 - 0
app/config/server.ts

@@ -10,6 +10,7 @@ declare global {
       VERCEL?: string;
       HIDE_USER_API_KEY?: string; // disable user's api key input
       DISABLE_GPT4?: string; // allow user to use gpt-4 or not
+      BUILD_MODE?: "standalone" | "export";
     }
   }
 }

+ 1 - 0
app/constant.ts

@@ -6,6 +6,7 @@ export const UPDATE_URL = `${REPO_URL}#keep-updated`;
 export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
 export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
 export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
+export const DEFAULT_API_HOST = "https://chatgpt.nextweb.fun/api/proxy";
 
 export enum Path {
   Home = "/",

+ 2 - 3
app/layout.tsx

@@ -3,8 +3,7 @@ import "./styles/globals.scss";
 import "./styles/markdown.scss";
 import "./styles/highlight.scss";
 import { getBuildConfig } from "./config/build";
-
-const buildConfig = getBuildConfig();
+import { getClientConfig } from "./config/client";
 
 export const metadata = {
   title: "ChatGPT Next Web",
@@ -32,7 +31,7 @@ export default function RootLayout({
   return (
     <html lang="en">
       <head>
-        <meta name="version" content={buildConfig.commitId} />
+        <meta name="config" content={JSON.stringify(getClientConfig())} />
         <link rel="manifest" href="/site.webmanifest"></link>
         <script src="/serviceWorkerRegister.js" defer></script>
       </head>

+ 4 - 0
app/locales/cn.ts

@@ -180,6 +180,10 @@ const cn = {
       SubTitle: "管理员已开启加密访问",
       Placeholder: "请输入访问密码",
     },
+    Endpoint: {
+      Title: "接口地址",
+      SubTitle: "除默认地址外,必须包含 http(s)://",
+    },
     Model: "模型 (model)",
     Temperature: {
       Title: "随机性 (temperature)",

+ 4 - 0
app/locales/en.ts

@@ -181,6 +181,10 @@ const en: RequiredLocaleType = {
       SubTitle: "Access control enabled",
       Placeholder: "Need Access Code",
     },
+    Endpoint: {
+      Title: "Endpoint",
+      SubTitle: "Custom endpoint must start with http(s)://",
+    },
     Model: "Model",
     Temperature: {
       Title: "Temperature",

+ 11 - 2
app/store/access.ts

@@ -1,9 +1,10 @@
 import { create } from "zustand";
 import { persist } from "zustand/middleware";
-import { StoreKey } from "../constant";
+import { DEFAULT_API_HOST, StoreKey } from "../constant";
 import { getHeaders } from "../client/api";
 import { BOT_HELLO } from "./chat";
 import { ALL_MODELS } from "./config";
+import { getClientConfig } from "../config/client";
 
 export interface AccessControlStore {
   accessCode: string;
@@ -15,6 +16,7 @@ export interface AccessControlStore {
 
   updateToken: (_: string) => void;
   updateCode: (_: string) => void;
+  updateOpenAiUrl: (_: string) => void;
   enabledAccessControl: () => boolean;
   isAuthorized: () => boolean;
   fetch: () => void;
@@ -22,6 +24,10 @@ export interface AccessControlStore {
 
 let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
 
+const DEFAULT_OPENAI_URL =
+  getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
+console.log("[API] default openai url", DEFAULT_OPENAI_URL);
+
 export const useAccessStore = create<AccessControlStore>()(
   persist(
     (set, get) => ({
@@ -29,7 +35,7 @@ export const useAccessStore = create<AccessControlStore>()(
       accessCode: "",
       needCode: true,
       hideUserApiKey: false,
-      openaiUrl: "/api/openai/",
+      openaiUrl: DEFAULT_OPENAI_URL,
 
       enabledAccessControl() {
         get().fetch();
@@ -42,6 +48,9 @@ export const useAccessStore = create<AccessControlStore>()(
       updateToken(token: string) {
         set(() => ({ token }));
       },
+      updateOpenAiUrl(url: string) {
+        set(() => ({ openaiUrl: url }));
+      },
       isAuthorized() {
         get().fetch();
 

+ 2 - 16
app/store/update.ts

@@ -2,7 +2,7 @@ import { create } from "zustand";
 import { persist } from "zustand/middleware";
 import { FETCH_COMMIT_URL, StoreKey } from "../constant";
 import { api } from "../client/api";
-import { showToast } from "../components/ui-lib";
+import { getClientConfig } from "../config/client";
 
 export interface UpdateStore {
   lastUpdate: number;
@@ -17,20 +17,6 @@ export interface UpdateStore {
   updateUsage: (force?: boolean) => Promise<void>;
 }
 
-function queryMeta(key: string, defaultValue?: string): string {
-  let ret: string;
-  if (document) {
-    const meta = document.head.querySelector(
-      `meta[name='${key}']`,
-    ) as HTMLMetaElement;
-    ret = meta?.content ?? "";
-  } else {
-    ret = defaultValue ?? "";
-  }
-
-  return ret;
-}
-
 const ONE_MINUTE = 60 * 1000;
 
 export const useUpdateStore = create<UpdateStore>()(
@@ -44,7 +30,7 @@ export const useUpdateStore = create<UpdateStore>()(
       version: "unknown",
 
       async getLatestVersion(force = false) {
-        set(() => ({ version: queryMeta("version") ?? "unknown" }));
+        set(() => ({ version: getClientConfig()?.commitId ?? "unknown" }));
 
         const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
         if (!force && !overTenMins) return;

+ 21 - 0
next.config.mjs

@@ -15,6 +15,27 @@ const nextConfig = {
 };
 
 if (mode !== "export") {
+  nextConfig.headers = async () => {
+    return [
+      {
+        source: "/:path*",
+        headers: [
+          { key: "Access-Control-Allow-Credentials", value: "true" },
+          { key: "Access-Control-Allow-Origin", value: "*" },
+          {
+            key: "Access-Control-Allow-Methods",
+            value: "GET,OPTIONS,PATCH,DELETE,POST,PUT",
+          },
+          {
+            key: "Access-Control-Allow-Headers",
+            value:
+              "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
+          },
+        ],
+      },
+    ];
+  };
+
   nextConfig.rewrites = async () => {
     const ret = [
       {