diff --git a/app/client/api.ts b/app/client/api.ts index fb829f97a39..8897d7a6648 100644 --- a/app/client/api.ts +++ b/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; } +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; diff --git a/app/components/home.tsx b/app/components/home.tsx index 96bcd28820c..16650228c32 100644 --- a/app/components/home.tsx +++ b/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 ; } diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 9029fdf2ca3..4f8379f59c7 100644 --- a/app/components/settings.tsx +++ b/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() { /> )} + + {!accessStore.hideUserApiKey ? ( + + + accessStore.updateOpenAiUrl(e.currentTarget.value) + } + > + + ) : null} diff --git a/app/config/build.ts b/app/config/build.ts index 79ed5d3e894..f294ef3f2ac 100644 --- a/app/config/build.ts +++ b/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; diff --git a/app/config/client.ts b/app/config/client.ts new file mode 100644 index 00000000000..da582a3e858 --- /dev/null +++ b/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; +} diff --git a/app/config/server.ts b/app/config/server.ts index b978e726e7b..f5fee71908e 100644 --- a/app/config/server.ts +++ b/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"; } } } diff --git a/app/constant.ts b/app/constant.ts index 0f805275311..6f31ad431d2 100644 --- a/app/constant.ts +++ b/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 = "/", diff --git a/app/layout.tsx b/app/layout.tsx index 37f5a9f1437..e7c14e307fd 100644 --- a/app/layout.tsx +++ b/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 ( - + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index d33ba101b8e..0dc5d354549 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -180,6 +180,10 @@ const cn = { SubTitle: "管理员已开启加密访问", Placeholder: "请输入访问密码", }, + Endpoint: { + Title: "接口地址", + SubTitle: "除默认地址外,必须包含 http(s)://", + }, Model: "模型 (model)", Temperature: { Title: "随机性 (temperature)", diff --git a/app/locales/en.ts b/app/locales/en.ts index 9c8bc2a7941..6d7174dc3e4 100644 --- a/app/locales/en.ts +++ b/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", diff --git a/app/store/access.ts b/app/store/access.ts index 91049846b1d..daefa0aad7a 100644 --- a/app/store/access.ts +++ b/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()( persist( (set, get) => ({ @@ -29,7 +35,7 @@ export const useAccessStore = create()( accessCode: "", needCode: true, hideUserApiKey: false, - openaiUrl: "/api/openai/", + openaiUrl: DEFAULT_OPENAI_URL, enabledAccessControl() { get().fetch(); @@ -42,6 +48,9 @@ export const useAccessStore = create()( updateToken(token: string) { set(() => ({ token })); }, + updateOpenAiUrl(url: string) { + set(() => ({ openaiUrl: url })); + }, isAuthorized() { get().fetch(); diff --git a/app/store/update.ts b/app/store/update.ts index 5a9bec9d71d..ca2ae70adcf 100644 --- a/app/store/update.ts +++ b/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; } -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()( @@ -44,7 +30,7 @@ export const useUpdateStore = create()( 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; diff --git a/next.config.mjs b/next.config.mjs index 7bb1436bfe5..b2a47deb2a7 100644 --- a/next.config.mjs +++ b/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 = [ { diff --git a/package.json b/package.json index c95f80876c0..60c6ac61ce1 100644 --- a/package.json +++ b/package.json @@ -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"