Bläddra i källkod

feat: dynamic config

Yidadaa 2 år sedan
förälder
incheckning
d6e6dd09f0
8 ändrade filer med 158 tillägg och 152 borttagningar
  1. 21 0
      app/api/config/route.ts
  2. 77 80
      app/components/home.tsx
  3. 3 4
      app/components/settings.tsx
  4. 0 42
      app/config/client.ts
  5. 2 15
      app/page.tsx
  6. 28 2
      app/store/access.ts
  7. 26 8
      app/store/update.ts
  8. 1 1
      middleware.ts

+ 21 - 0
app/api/config/route.ts

@@ -0,0 +1,21 @@
+import { NextRequest, NextResponse } from "next/server";
+
+import { getServerSideConfig } from "../../config/server";
+
+const serverConfig = getServerSideConfig();
+
+// Danger! Don not write any secret value here!
+// 警告!不要在这里写入任何敏感信息!
+const DANGER_CONFIG = {
+  needCode: serverConfig.needCode,
+};
+
+declare global {
+  type DangerConfig = typeof DANGER_CONFIG;
+}
+
+export async function POST(req: NextRequest) {
+  return NextResponse.json({
+    needCode: serverConfig.needCode,
+  });
+}

+ 77 - 80
app/components/home.tsx

@@ -2,13 +2,7 @@
 
 require("../polyfill");
 
-import {
-  useState,
-  useEffect,
-  useRef,
-  useCallback,
-  MouseEventHandler,
-} from "react";
+import { useState, useEffect, useRef } from "react";
 
 import { IconButton } from "./button";
 import styles from "./home.module.scss";
@@ -30,7 +24,6 @@ import { Chat } from "./chat";
 import dynamic from "next/dynamic";
 import { REPO_URL } from "../constant";
 import { ErrorBoundary } from "./error";
-import { useDebounce } from "use-debounce";
 
 export function Loading(props: { noLogo?: boolean }) {
   return (
@@ -165,96 +158,100 @@ function _Home() {
   }
 
   return (
-    <div
-      className={`${
-        config.tightBorder && !isMobileScreen()
-          ? styles["tight-container"]
-          : styles.container
-      }`}
-    >
+    <>
       <div
-        className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
+        className={`${
+          config.tightBorder && !isMobileScreen()
+            ? styles["tight-container"]
+            : styles.container
+        }`}
       >
-        <div className={styles["sidebar-header"]}>
-          <div className={styles["sidebar-title"]}>ChatGPT Next</div>
-          <div className={styles["sidebar-sub-title"]}>
-            Build your own AI assistant.
-          </div>
-          <div className={styles["sidebar-logo"]}>
-            <ChatGptIcon />
-          </div>
-        </div>
-
         <div
-          className={styles["sidebar-body"]}
-          onClick={() => {
-            setOpenSettings(false);
-            setShowSideBar(false);
-          }}
+          className={
+            styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`
+          }
         >
-          <ChatList />
-        </div>
+          <div className={styles["sidebar-header"]}>
+            <div className={styles["sidebar-title"]}>ChatGPT Next</div>
+            <div className={styles["sidebar-sub-title"]}>
+              Build your own AI assistant.
+            </div>
+            <div className={styles["sidebar-logo"]}>
+              <ChatGptIcon />
+            </div>
+          </div>
 
-        <div className={styles["sidebar-tail"]}>
-          <div className={styles["sidebar-actions"]}>
-            <div className={styles["sidebar-action"] + " " + styles.mobile}>
-              <IconButton
-                icon={<CloseIcon />}
-                onClick={chatStore.deleteSession}
-              />
+          <div
+            className={styles["sidebar-body"]}
+            onClick={() => {
+              setOpenSettings(false);
+              setShowSideBar(false);
+            }}
+          >
+            <ChatList />
+          </div>
+
+          <div className={styles["sidebar-tail"]}>
+            <div className={styles["sidebar-actions"]}>
+              <div className={styles["sidebar-action"] + " " + styles.mobile}>
+                <IconButton
+                  icon={<CloseIcon />}
+                  onClick={chatStore.deleteSession}
+                />
+              </div>
+              <div className={styles["sidebar-action"]}>
+                <IconButton
+                  icon={<SettingsIcon />}
+                  onClick={() => {
+                    setOpenSettings(true);
+                    setShowSideBar(false);
+                  }}
+                  shadow
+                />
+              </div>
+              <div className={styles["sidebar-action"]}>
+                <a href={REPO_URL} target="_blank">
+                  <IconButton icon={<GithubIcon />} shadow />
+                </a>
+              </div>
             </div>
-            <div className={styles["sidebar-action"]}>
+            <div>
               <IconButton
-                icon={<SettingsIcon />}
+                icon={<AddIcon />}
+                text={Locale.Home.NewChat}
                 onClick={() => {
-                  setOpenSettings(true);
+                  createNewSession();
                   setShowSideBar(false);
                 }}
                 shadow
               />
             </div>
-            <div className={styles["sidebar-action"]}>
-              <a href={REPO_URL} target="_blank">
-                <IconButton icon={<GithubIcon />} shadow />
-              </a>
-            </div>
           </div>
-          <div>
-            <IconButton
-              icon={<AddIcon />}
-              text={Locale.Home.NewChat}
-              onClick={() => {
-                createNewSession();
-                setShowSideBar(false);
+
+          <div
+            className={styles["sidebar-drag"]}
+            onMouseDown={(e) => onDragMouseDown(e as any)}
+          ></div>
+        </div>
+
+        <div className={styles["window-content"]}>
+          {openSettings ? (
+            <Settings
+              closeSettings={() => {
+                setOpenSettings(false);
+                setShowSideBar(true);
               }}
-              shadow
             />
-          </div>
+          ) : (
+            <Chat
+              key="chat"
+              showSideBar={() => setShowSideBar(true)}
+              sideBarShowing={showSideBar}
+            />
+          )}
         </div>
-
-        <div
-          className={styles["sidebar-drag"]}
-          onMouseDown={(e) => onDragMouseDown(e as any)}
-        ></div>
-      </div>
-
-      <div className={styles["window-content"]}>
-        {openSettings ? (
-          <Settings
-            closeSettings={() => {
-              setOpenSettings(false);
-              setShowSideBar(true);
-            }}
-          />
-        ) : (
-          <Chat
-            key="chat"
-            showSideBar={() => setShowSideBar(true)}
-            sideBarShowing={showSideBar}
-          />
-        )}
       </div>
-    </div>
+    </>
   );
 }
 

+ 3 - 4
app/components/settings.tsx

@@ -33,7 +33,6 @@ import { SearchService, usePromptStore } from "../store/prompt";
 import { requestUsage } from "../requests";
 import { ErrorBoundary } from "./error";
 import { InputRange } from "./input-range";
-import { getClientSideConfig } from "../config/client";
 
 function SettingItem(props: {
   title: string;
@@ -89,13 +88,13 @@ export function Settings(props: { closeSettings: () => void }) {
 
   const updateStore = useUpdateStore();
   const [checkingUpdate, setCheckingUpdate] = useState(false);
-  const currentVersion = getClientSideConfig()?.version;
-  const remoteId = updateStore.remoteId;
+  const currentVersion = updateStore.version;
+  const remoteId = updateStore.remoteVersion;
   const hasNewVersion = currentVersion !== remoteId;
 
   function checkUpdate(force = false) {
     setCheckingUpdate(true);
-    updateStore.getLatestCommitId(force).then(() => {
+    updateStore.getLatestVersion(force).then(() => {
       setCheckingUpdate(false);
     });
   }

+ 0 - 42
app/config/client.ts

@@ -1,42 +0,0 @@
-import { RUNTIME_CONFIG_DOM } from "../constant";
-
-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;
-}
-
-export function getClientSideConfig() {
-  if (typeof window === "undefined") {
-    throw Error(
-      "[Client Config] you are importing a browser-only module outside of browser",
-    );
-  }
-
-  const dom = document.getElementById(RUNTIME_CONFIG_DOM);
-
-  if (!dom) {
-    throw Error("[Config] Dont get config before page loading!");
-  }
-
-  try {
-    const fromServerConfig = JSON.parse(dom.innerText) as DangerConfig;
-    const fromBuildConfig = {
-      version: queryMeta("version"),
-    };
-    return {
-      ...fromServerConfig,
-      ...fromBuildConfig,
-    };
-  } catch (e) {
-    console.error("[Config] failed to parse client config");
-  }
-}

+ 2 - 15
app/page.tsx

@@ -1,27 +1,14 @@
 import { Analytics } from "@vercel/analytics/react";
 
 import { Home } from "./components/home";
+
 import { getServerSideConfig } from "./config/server";
-import { RUNTIME_CONFIG_DOM } from "./constant";
 
 const serverConfig = getServerSideConfig();
 
-// Danger! Don not write any secret value here!
-// 警告!不要在这里写入任何敏感信息!
-const DANGER_CONFIG = {
-  needCode: serverConfig?.needCode,
-};
-
-declare global {
-  type DangerConfig = typeof DANGER_CONFIG;
-}
-
-export default function App() {
+export default async function App() {
   return (
     <>
-      <div style={{ display: "none" }} id={RUNTIME_CONFIG_DOM}>
-        {JSON.stringify(DANGER_CONFIG)}
-      </div>
       <Home />
       {serverConfig?.isVercel && <Analytics />}
     </>

+ 28 - 2
app/store/access.ts

@@ -1,26 +1,33 @@
 import { create } from "zustand";
 import { persist } from "zustand/middleware";
-import { getClientSideConfig } from "../config/client";
 
 export interface AccessControlStore {
   accessCode: string;
   token: string;
 
+  needCode: boolean;
+
   updateToken: (_: string) => void;
   updateCode: (_: string) => void;
   enabledAccessControl: () => boolean;
   isAuthorized: () => boolean;
+  fetch: () => void;
 }
 
 export const ACCESS_KEY = "access-control";
 
+let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
+
 export const useAccessStore = create<AccessControlStore>()(
   persist(
     (set, get) => ({
       token: "",
       accessCode: "",
+      needCode: true,
       enabledAccessControl() {
-        return !!getClientSideConfig()?.needCode;
+        get().fetch();
+
+        return get().needCode;
       },
       updateCode(code: string) {
         set((state) => ({ accessCode: code }));
@@ -34,6 +41,25 @@ export const useAccessStore = create<AccessControlStore>()(
           !!get().token || !!get().accessCode || !get().enabledAccessControl()
         );
       },
+      fetch() {
+        if (fetchState > 0) return;
+        fetchState = 1;
+        fetch("/api/config", {
+          method: "post",
+          body: null,
+        })
+          .then((res) => res.json())
+          .then((res: DangerConfig) => {
+            console.log("[Config] got config from server", res);
+            set(() => ({ ...res }));
+          })
+          .catch(() => {
+            console.error("[Config] failed to fetch config");
+          })
+          .finally(() => {
+            fetchState = 2;
+          });
+      },
     }),
     {
       name: ACCESS_KEY,

+ 26 - 8
app/store/update.ts

@@ -1,28 +1,46 @@
 import { create } from "zustand";
 import { persist } from "zustand/middleware";
-import { getClientSideConfig } from "../config/client";
 import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
 
 export interface UpdateStore {
   lastUpdate: number;
-  remoteId: string;
+  remoteVersion: string;
 
-  getLatestCommitId: (force: boolean) => Promise<string>;
+  version: string;
+  getLatestVersion: (force: boolean) => Promise<string>;
 }
 
 export const UPDATE_KEY = "chat-update";
 
+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;
+}
+
 export const useUpdateStore = create<UpdateStore>()(
   persist(
     (set, get) => ({
       lastUpdate: 0,
-      remoteId: "",
+      remoteVersion: "",
+
+      version: "unknown",
+
+      async getLatestVersion(force = false) {
+        set(() => ({ version: queryMeta("version") }));
 
-      async getLatestCommitId(force = false) {
         const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
         const shouldFetch = force || overTenMins;
         if (!shouldFetch) {
-          return getClientSideConfig()?.version ?? "";
+          return get().version ?? "unknown";
         }
 
         try {
@@ -32,13 +50,13 @@ export const useUpdateStore = create<UpdateStore>()(
           const remoteId = (data[0].sha as string).substring(0, 7);
           set(() => ({
             lastUpdate: Date.now(),
-            remoteId,
+            remoteVersion: remoteId,
           }));
           console.log("[Got Upstream] ", remoteId);
           return remoteId;
         } catch (error) {
           console.error("[Fetch Upstream Commit Id]", error);
-          return getClientSideConfig()?.version ?? "";
+          return get().version ?? "";
         }
       },
     }),

+ 1 - 1
middleware.ts

@@ -32,7 +32,7 @@ export function middleware(req: NextRequest) {
 
   // inject api key
   if (!token) {
-    const apiKey = process.env.OPENAI_API_KEY;
+    const apiKey = serverConfig.apiKey;
     if (apiKey) {
       console.log("[Auth] set system token");
       req.headers.set("token", apiKey);