feat: dynamic config

This commit is contained in:
Yidadaa 2023-04-11 02:54:31 +08:00
parent 9b61cb1335
commit d6e6dd09f0
8 changed files with 160 additions and 154 deletions

21
app/api/config/route.ts Normal file
View File

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

View File

@ -2,13 +2,7 @@
require("../polyfill"); require("../polyfill");
import { import { useState, useEffect, useRef } from "react";
useState,
useEffect,
useRef,
useCallback,
MouseEventHandler,
} from "react";
import { IconButton } from "./button"; import { IconButton } from "./button";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
@ -30,7 +24,6 @@ import { Chat } from "./chat";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { REPO_URL } from "../constant"; import { REPO_URL } from "../constant";
import { ErrorBoundary } from "./error"; import { ErrorBoundary } from "./error";
import { useDebounce } from "use-debounce";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (
@ -165,6 +158,7 @@ function _Home() {
} }
return ( return (
<>
<div <div
className={`${ className={`${
config.tightBorder && !isMobileScreen() config.tightBorder && !isMobileScreen()
@ -173,7 +167,9 @@ function _Home() {
}`} }`}
> >
<div <div
className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`} className={
styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`
}
> >
<div className={styles["sidebar-header"]}> <div className={styles["sidebar-header"]}>
<div className={styles["sidebar-title"]}>ChatGPT Next</div> <div className={styles["sidebar-title"]}>ChatGPT Next</div>
@ -255,6 +251,7 @@ function _Home() {
)} )}
</div> </div>
</div> </div>
</>
); );
} }

View File

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

View File

@ -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");
}
}

View File

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

View File

@ -1,26 +1,33 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { getClientSideConfig } from "../config/client";
export interface AccessControlStore { export interface AccessControlStore {
accessCode: string; accessCode: string;
token: string; token: string;
needCode: boolean;
updateToken: (_: string) => void; updateToken: (_: string) => void;
updateCode: (_: string) => void; updateCode: (_: string) => void;
enabledAccessControl: () => boolean; enabledAccessControl: () => boolean;
isAuthorized: () => boolean; isAuthorized: () => boolean;
fetch: () => void;
} }
export const ACCESS_KEY = "access-control"; export const ACCESS_KEY = "access-control";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
export const useAccessStore = create<AccessControlStore>()( export const useAccessStore = create<AccessControlStore>()(
persist( persist(
(set, get) => ({ (set, get) => ({
token: "", token: "",
accessCode: "", accessCode: "",
needCode: true,
enabledAccessControl() { enabledAccessControl() {
return !!getClientSideConfig()?.needCode; get().fetch();
return get().needCode;
}, },
updateCode(code: string) { updateCode(code: string) {
set((state) => ({ accessCode: code })); set((state) => ({ accessCode: code }));
@ -34,6 +41,25 @@ export const useAccessStore = create<AccessControlStore>()(
!!get().token || !!get().accessCode || !get().enabledAccessControl() !!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, name: ACCESS_KEY,

View File

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

View File

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