mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	refactor: build/runtime/client configs
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -26,13 +26,14 @@ 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";
 | 
			
		||||
import { requestUsage } from "../requests";
 | 
			
		||||
import { ErrorBoundary } from "./error";
 | 
			
		||||
import { InputRange } from "./input-range";
 | 
			
		||||
import { getClientSideConfig } from "../config/client";
 | 
			
		||||
 | 
			
		||||
function SettingItem(props: {
 | 
			
		||||
  title: string;
 | 
			
		||||
@@ -88,9 +89,9 @@ export function Settings(props: { closeSettings: () => void }) {
 | 
			
		||||
 | 
			
		||||
  const updateStore = useUpdateStore();
 | 
			
		||||
  const [checkingUpdate, setCheckingUpdate] = useState(false);
 | 
			
		||||
  const currentId = getCurrentVersion();
 | 
			
		||||
  const currentVersion = getClientSideConfig()?.version;
 | 
			
		||||
  const remoteId = updateStore.remoteId;
 | 
			
		||||
  const hasNewVersion = currentId !== remoteId;
 | 
			
		||||
  const hasNewVersion = currentVersion !== remoteId;
 | 
			
		||||
 | 
			
		||||
  function checkUpdate(force = false) {
 | 
			
		||||
    setCheckingUpdate(true);
 | 
			
		||||
@@ -224,7 +225,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
									
								
								app/config/build.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/config/build.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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
									
								
								app/config/client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/config/client.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
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");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								app/config/server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/config/server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@@ -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";
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								app/page.tsx
									
									
									
									
									
								
							@@ -1,12 +1,29 @@
 | 
			
		||||
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() {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div style={{ display: "none" }} id={RUNTIME_CONFIG_DOM}>
 | 
			
		||||
        {JSON.stringify(DANGER_CONFIG)}
 | 
			
		||||
      </div>
 | 
			
		||||
      <Home />
 | 
			
		||||
      <Analytics />
 | 
			
		||||
      {serverConfig?.isVercel && <Analytics />}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { create } from "zustand";
 | 
			
		||||
import { persist } from "zustand/middleware";
 | 
			
		||||
import { queryMeta } from "../utils";
 | 
			
		||||
import { getClientSideConfig } from "../config/client";
 | 
			
		||||
 | 
			
		||||
export interface AccessControlStore {
 | 
			
		||||
  accessCode: string;
 | 
			
		||||
@@ -20,7 +20,7 @@ export const useAccessStore = create<AccessControlStore>()(
 | 
			
		||||
      token: "",
 | 
			
		||||
      accessCode: "",
 | 
			
		||||
      enabledAccessControl() {
 | 
			
		||||
        return queryMeta("access") === "enabled";
 | 
			
		||||
        return !!getClientSideConfig()?.needCode;
 | 
			
		||||
      },
 | 
			
		||||
      updateCode(code: string) {
 | 
			
		||||
        set((state) => ({ accessCode: code }));
 | 
			
		||||
@@ -30,7 +30,9 @@ 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()
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { create } from "zustand";
 | 
			
		||||
import { persist } from "zustand/middleware";
 | 
			
		||||
import { getClientSideConfig } from "../config/client";
 | 
			
		||||
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
 | 
			
		||||
import { getCurrentVersion } from "../utils";
 | 
			
		||||
 | 
			
		||||
export interface UpdateStore {
 | 
			
		||||
  lastUpdate: number;
 | 
			
		||||
@@ -22,7 +22,7 @@ export const useUpdateStore = create<UpdateStore>()(
 | 
			
		||||
        const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
 | 
			
		||||
        const shouldFetch = force || overTenMins;
 | 
			
		||||
        if (!shouldFetch) {
 | 
			
		||||
          return getCurrentVersion();
 | 
			
		||||
          return getClientSideConfig()?.version ?? "";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
@@ -38,7 +38,7 @@ export const useUpdateStore = create<UpdateStore>()(
 | 
			
		||||
          return remoteId;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error("[Fetch Upstream Commit Id]", error);
 | 
			
		||||
          return getCurrentVersion();
 | 
			
		||||
          return getClientSideConfig()?.version ?? "";
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								app/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										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`;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user