mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Merge pull request #1927 from Yidadaa/bugfix-0613
feat: #1000 ready to support client-side only
This commit is contained in:
		@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 />;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
								app/config/client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/config/client.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = "/",
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -180,6 +180,10 @@ const cn = {
 | 
			
		||||
      SubTitle: "管理员已开启加密访问",
 | 
			
		||||
      Placeholder: "请输入访问密码",
 | 
			
		||||
    },
 | 
			
		||||
    Endpoint: {
 | 
			
		||||
      Title: "接口地址",
 | 
			
		||||
      SubTitle: "除默认地址外,必须包含 http(s)://",
 | 
			
		||||
    },
 | 
			
		||||
    Model: "模型 (model)",
 | 
			
		||||
    Temperature: {
 | 
			
		||||
      Title: "随机性 (temperature)",
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = [
 | 
			
		||||
      {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "export": "BUILD_MODE=export yarn build",
 | 
			
		||||
    "prompts": "node ./scripts/fetch-prompts.mjs",
 | 
			
		||||
    "prepare": "husky install",
 | 
			
		||||
    "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user