Browse Source

Merge pull request #697 from Yidadaa/bugfix-0410

fix: runtime config and proxy fix
Yifei Zhang 1 year ago
parent
commit
0f739f442e

+ 1 - 2
Dockerfile

@@ -17,7 +17,6 @@ RUN apk update && apk add --no-cache git
 
 ENV OPENAI_API_KEY=""
 ENV CODE=""
-ARG DOCKER=true
 
 WORKDIR /app
 COPY --from=deps /app/node_modules ./node_modules
@@ -46,7 +45,7 @@ CMD if [ -n "$PROXY_URL" ]; then \
         host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
         port=$(echo $PROXY_URL | cut -d: -f3); \
         conf=/etc/proxychains.conf; \
-        echo "strict_chain" >> $conf; \
+        echo "strict_chain" > $conf; \
         echo "proxy_dns" >> $conf; \
         echo "remote_dns_subnet 224" >> $conf; \
         echo "tcp_read_time_out 15000" >> $conf; \

+ 0 - 17
app/api/access.ts

@@ -1,17 +0,0 @@
-import md5 from "spark-md5";
-
-export function getAccessCodes(): Set<string> {
-  const code = process.env.CODE;
-
-  try {
-    const codes = (code?.split(",") ?? [])
-      .filter((v) => !!v)
-      .map((v) => md5.hash(v.trim()));
-    return new Set(codes);
-  } catch (e) {
-    return new Set();
-  }
-}
-
-export const ACCESS_CODES = getAccessCodes();
-export const IS_IN_DOCKER = process.env.DOCKER;

+ 1 - 1
app/api/chat-stream/route.ts

@@ -40,7 +40,7 @@ async function createStream(req: NextRequest) {
 
       const parser = createParser(onParse);
       for await (const chunk of res.body as any) {
-        parser.feed(decoder.decode(chunk));
+        parser.feed(decoder.decode(chunk, { stream: true }));
       }
     },
   });

+ 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,
+  });
+}

+ 1 - 8
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 (

+ 6 - 6
app/components/settings.tsx

@@ -26,7 +26,7 @@ import {
 import { Avatar } from "./chat";
 
 import Locale, { AllLangs, changeLang, getLang } from "../locales";
-import { getCurrentVersion, getEmojiUrl } from "../utils";
+import { getEmojiUrl } from "../utils";
 import Link from "next/link";
 import { UPDATE_URL } from "../constant";
 import { SearchService, usePromptStore } from "../store/prompt";
@@ -88,13 +88,13 @@ export function Settings(props: { closeSettings: () => void }) {
 
   const updateStore = useUpdateStore();
   const [checkingUpdate, setCheckingUpdate] = useState(false);
-  const currentId = getCurrentVersion();
-  const remoteId = updateStore.remoteId;
-  const hasNewVersion = currentId !== 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);
     });
   }
@@ -224,7 +224,7 @@ export function Settings(props: { closeSettings: () => void }) {
           </SettingItem>
 
           <SettingItem
-            title={Locale.Settings.Update.Version(currentId)}
+            title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
             subTitle={
               checkingUpdate
                 ? Locale.Settings.Update.IsChecking

+ 27 - 0
app/config/build.ts

@@ -0,0 +1,27 @@
+const COMMIT_ID: string = (() => {
+  try {
+    const childProcess = require("child_process");
+    return (
+      childProcess
+        // .execSync("git describe --tags --abbrev=0")
+        .execSync("git rev-parse --short HEAD")
+        .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(
+      "[Server Config] you are importing a nodejs-only module outside of nodejs",
+    );
+  }
+
+  return {
+    commitId: COMMIT_ID,
+  };
+};

+ 42 - 0
app/config/server.ts

@@ -0,0 +1,42 @@
+import md5 from "spark-md5";
+
+declare global {
+  namespace NodeJS {
+    interface ProcessEnv {
+      OPENAI_API_KEY?: string;
+      CODE?: string;
+      PROXY_URL?: string;
+      VERCEL?: string;
+    }
+  }
+}
+
+const ACCESS_CODES = (function getAccessCodes(): Set<string> {
+  const code = process.env.CODE;
+
+  try {
+    const codes = (code?.split(",") ?? [])
+      .filter((v) => !!v)
+      .map((v) => md5.hash(v.trim()));
+    return new Set(codes);
+  } catch (e) {
+    return new Set();
+  }
+})();
+
+export const getServerSideConfig = () => {
+  if (typeof process === "undefined") {
+    throw Error(
+      "[Server Config] you are importing a nodejs-only module outside of nodejs",
+    );
+  }
+
+  return {
+    apiKey: process.env.OPENAI_API_KEY,
+    code: process.env.CODE,
+    codes: ACCESS_CODES,
+    needCode: ACCESS_CODES.size > 0,
+    proxyUrl: process.env.PROXY_URL,
+    isVercel: !!process.env.VERCEL,
+  };
+};

+ 1 - 0
app/constant.ts

@@ -5,3 +5,4 @@ export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
 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";

+ 3 - 28
app/layout.tsx

@@ -2,19 +2,9 @@
 import "./styles/globals.scss";
 import "./styles/markdown.scss";
 import "./styles/highlight.scss";
-import process from "child_process";
-import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
+import { getBuildConfig } from "./config/build";
 
-let COMMIT_ID: string | undefined;
-try {
-  COMMIT_ID = process
-    // .execSync("git describe --tags --abbrev=0")
-    .execSync("git rev-parse --short HEAD")
-    .toString()
-    .trim();
-} catch (e) {
-  console.error("No git or not from git repo.");
-}
+const buildConfig = getBuildConfig();
 
 export const metadata = {
   title: "ChatGPT Next Web",
@@ -26,21 +16,6 @@ export const metadata = {
   themeColor: "#fafafa",
 };
 
-function Meta() {
-  const metas = {
-    version: COMMIT_ID ?? "unknown",
-    access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
-  };
-
-  return (
-    <>
-      {Object.entries(metas).map(([k, v]) => (
-        <meta name={k} content={v} key={k} />
-      ))}
-    </>
-  );
-}
-
 export default function RootLayout({
   children,
 }: {
@@ -58,7 +33,7 @@ export default function RootLayout({
           content="#151515"
           media="(prefers-color-scheme: dark)"
         />
-        <Meta />
+        <meta name="version" content={buildConfig.commitId} />
         <link rel="manifest" href="/site.webmanifest"></link>
         <link rel="preconnect" href="https://fonts.googleapis.com"></link>
         <link rel="preconnect" href="https://fonts.gstatic.com"></link>

+ 6 - 2
app/page.tsx

@@ -2,11 +2,15 @@ import { Analytics } from "@vercel/analytics/react";
 
 import { Home } from "./components/home";
 
-export default function App() {
+import { getServerSideConfig } from "./config/server";
+
+const serverConfig = getServerSideConfig();
+
+export default async function App() {
   return (
     <>
       <Home />
-      <Analytics />
+      {serverConfig?.isVercel && <Analytics />}
     </>
   );
 }

+ 1 - 1
app/requests.ts

@@ -171,7 +171,7 @@ export async function requestChatStream(
         const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
         const content = await reader?.read();
         clearTimeout(resTimeoutId);
-        const text = decoder.decode(content?.value);
+        const text = decoder.decode(content?.value, { stream: true });
         responseText += text;
 
         const done = !content || content.done;

+ 31 - 3
app/store/access.ts

@@ -1,26 +1,33 @@
 import { create } from "zustand";
 import { persist } from "zustand/middleware";
-import { queryMeta } from "../utils";
 
 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 queryMeta("access") === "enabled";
+        get().fetch();
+
+        return get().needCode;
       },
       updateCode(code: string) {
         set((state) => ({ accessCode: code }));
@@ -30,7 +37,28 @@ export const useAccessStore = create<AccessControlStore>()(
       },
       isAuthorized() {
         // has token or has code or disabled access control
-        return !!get().token || !!get().accessCode || !get().enabledAccessControl();
+        return (
+          !!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;
+          });
       },
     }),
     {

+ 26 - 8
app/store/update.ts

@@ -1,28 +1,46 @@
 import { create } from "zustand";
 import { persist } from "zustand/middleware";
 import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
-import { getCurrentVersion } from "../utils";
 
 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 getCurrentVersion();
+          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 getCurrentVersion();
+          return get().version ?? "";
         }
       },
     }),

+ 0 - 25
app/utils.ts

@@ -69,31 +69,6 @@ export function selectOrCopy(el: HTMLElement, content: string) {
   return true;
 }
 
-export 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;
-}
-
-let currentId: string;
-export function getCurrentVersion() {
-  if (currentId) {
-    return currentId;
-  }
-
-  currentId = queryMeta("version");
-
-  return currentId;
-}
-
 export function getEmojiUrl(unified: string, style: EmojiStyle) {
   return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
 }

+ 6 - 4
middleware.ts

@@ -1,21 +1,23 @@
 import { NextRequest, NextResponse } from "next/server";
-import { ACCESS_CODES } from "./app/api/access";
+import { getServerSideConfig } from "./app/config/server";
 import md5 from "spark-md5";
 
 export const config = {
   matcher: ["/api/openai", "/api/chat-stream"],
 };
 
+const serverConfig = getServerSideConfig();
+
 export function middleware(req: NextRequest) {
   const accessCode = req.headers.get("access-code");
   const token = req.headers.get("token");
   const hashedCode = md5.hash(accessCode ?? "").trim();
 
-  console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
+  console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
   console.log("[Auth] got access code:", accessCode);
   console.log("[Auth] hashed access code:", hashedCode);
 
-  if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
+  if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
     return NextResponse.json(
       {
         error: true,
@@ -30,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);

+ 3 - 6
next.config.js

@@ -8,14 +8,11 @@ const nextConfig = {
     config.module.rules.push({
       test: /\.svg$/,
       use: ["@svgr/webpack"],
-    }); // 针对 SVG 的处理规则
+    });
 
     return config;
-  }
+  },
+  output: "standalone",
 };
 
-if (process.env.DOCKER) {
-  nextConfig.output = 'standalone'
-}
-
 module.exports = nextConfig;