Merge pull request #1927 from Yidadaa/bugfix-0613

feat: #1000 ready to support client-side only
This commit is contained in:
Yifei Zhang 2023-06-14 00:48:32 +08:00 committed by GitHub
commit 426ce7fd35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 134 additions and 36 deletions

View File

@ -1,5 +1,5 @@
import { ACCESS_CODE_PREFIX } from "../constant"; import { ACCESS_CODE_PREFIX } from "../constant";
import { ChatMessage, ModelConfig, ModelType, useAccessStore } from "../store"; import { ChatMessage, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai"; import { ChatGPTApi } from "./platforms/openai";
export const ROLES = ["system", "user", "assistant"] as const; export const ROLES = ["system", "user", "assistant"] as const;
@ -42,6 +42,27 @@ export abstract class LLMApi {
abstract usage(): Promise<LLMUsage>; 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 { export class ClientApi {
public llm: LLMApi; public llm: LLMApi;

View File

@ -24,6 +24,7 @@ import {
import { SideBar } from "./sidebar"; import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config"; import { useAppConfig } from "../store/config";
import { AuthPage } from "./auth"; import { AuthPage } from "./auth";
import { getClientConfig } from "../config/client";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (
@ -147,6 +148,10 @@ function Screen() {
export function Home() { export function Home() {
useSwitchTheme(); useSwitchTheme();
useEffect(() => {
console.log("[Config] got config from build time", getClientConfig());
}, []);
if (!useHasHydrated()) { if (!useHasHydrated()) {
return <Loading />; return <Loading />;
} }

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react"; import { useState, useEffect, useMemo } from "react";
import styles from "./settings.module.scss"; import styles from "./settings.module.scss";
@ -45,6 +45,7 @@ import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Avatar, AvatarPicker } from "./emoji"; import { Avatar, AvatarPicker } from "./emoji";
import { getClientConfig } from "../config/client";
function EditPromptModal(props: { id: number; onClose: () => void }) { function EditPromptModal(props: { id: number; onClose: () => void }) {
const promptStore = usePromptStore(); const promptStore = usePromptStore();
@ -541,6 +542,21 @@ export function Settings() {
/> />
)} )}
</ListItem> </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>
<List> <List>

View File

@ -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 = () => { export const getBuildConfig = () => {
if (typeof process === "undefined") { if (typeof process === "undefined") {
throw Error( 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 { return {
commitId: COMMIT_ID, commitId: COMMIT_ID,
buildMode: process.env.BUILD_MODE ?? "standalone",
}; };
}; };
export type BuildConfig = ReturnType<typeof getBuildConfig>;

27
app/config/client.ts Normal file
View 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;
}

View File

@ -10,6 +10,7 @@ declare global {
VERCEL?: string; VERCEL?: string;
HIDE_USER_API_KEY?: string; // disable user's api key input HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not DISABLE_GPT4?: string; // allow user to use gpt-4 or not
BUILD_MODE?: "standalone" | "export";
} }
} }
} }

View File

@ -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_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 FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const DEFAULT_API_HOST = "https://chatgpt.nextweb.fun/api/proxy";
export enum Path { export enum Path {
Home = "/", Home = "/",

View File

@ -3,8 +3,7 @@ import "./styles/globals.scss";
import "./styles/markdown.scss"; import "./styles/markdown.scss";
import "./styles/highlight.scss"; import "./styles/highlight.scss";
import { getBuildConfig } from "./config/build"; import { getBuildConfig } from "./config/build";
import { getClientConfig } from "./config/client";
const buildConfig = getBuildConfig();
export const metadata = { export const metadata = {
title: "ChatGPT Next Web", title: "ChatGPT Next Web",
@ -32,7 +31,7 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<meta name="version" content={buildConfig.commitId} /> <meta name="config" content={JSON.stringify(getClientConfig())} />
<link rel="manifest" href="/site.webmanifest"></link> <link rel="manifest" href="/site.webmanifest"></link>
<script src="/serviceWorkerRegister.js" defer></script> <script src="/serviceWorkerRegister.js" defer></script>
</head> </head>

View File

@ -180,6 +180,10 @@ const cn = {
SubTitle: "管理员已开启加密访问", SubTitle: "管理员已开启加密访问",
Placeholder: "请输入访问密码", Placeholder: "请输入访问密码",
}, },
Endpoint: {
Title: "接口地址",
SubTitle: "除默认地址外,必须包含 http(s)://",
},
Model: "模型 (model)", Model: "模型 (model)",
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",

View File

@ -181,6 +181,10 @@ const en: RequiredLocaleType = {
SubTitle: "Access control enabled", SubTitle: "Access control enabled",
Placeholder: "Need Access Code", Placeholder: "Need Access Code",
}, },
Endpoint: {
Title: "Endpoint",
SubTitle: "Custom endpoint must start with http(s)://",
},
Model: "Model", Model: "Model",
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",

View File

@ -1,9 +1,10 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { StoreKey } from "../constant"; import { DEFAULT_API_HOST, StoreKey } from "../constant";
import { getHeaders } from "../client/api"; import { getHeaders } from "../client/api";
import { BOT_HELLO } from "./chat"; import { BOT_HELLO } from "./chat";
import { ALL_MODELS } from "./config"; import { ALL_MODELS } from "./config";
import { getClientConfig } from "../config/client";
export interface AccessControlStore { export interface AccessControlStore {
accessCode: string; accessCode: string;
@ -15,6 +16,7 @@ export interface AccessControlStore {
updateToken: (_: string) => void; updateToken: (_: string) => void;
updateCode: (_: string) => void; updateCode: (_: string) => void;
updateOpenAiUrl: (_: string) => void;
enabledAccessControl: () => boolean; enabledAccessControl: () => boolean;
isAuthorized: () => boolean; isAuthorized: () => boolean;
fetch: () => void; fetch: () => void;
@ -22,6 +24,10 @@ export interface AccessControlStore {
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done 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>()( export const useAccessStore = create<AccessControlStore>()(
persist( persist(
(set, get) => ({ (set, get) => ({
@ -29,7 +35,7 @@ export const useAccessStore = create<AccessControlStore>()(
accessCode: "", accessCode: "",
needCode: true, needCode: true,
hideUserApiKey: false, hideUserApiKey: false,
openaiUrl: "/api/openai/", openaiUrl: DEFAULT_OPENAI_URL,
enabledAccessControl() { enabledAccessControl() {
get().fetch(); get().fetch();
@ -42,6 +48,9 @@ export const useAccessStore = create<AccessControlStore>()(
updateToken(token: string) { updateToken(token: string) {
set(() => ({ token })); set(() => ({ token }));
}, },
updateOpenAiUrl(url: string) {
set(() => ({ openaiUrl: url }));
},
isAuthorized() { isAuthorized() {
get().fetch(); get().fetch();

View File

@ -2,7 +2,7 @@ import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, StoreKey } from "../constant"; import { FETCH_COMMIT_URL, StoreKey } from "../constant";
import { api } from "../client/api"; import { api } from "../client/api";
import { showToast } from "../components/ui-lib"; import { getClientConfig } from "../config/client";
export interface UpdateStore { export interface UpdateStore {
lastUpdate: number; lastUpdate: number;
@ -17,20 +17,6 @@ export interface UpdateStore {
updateUsage: (force?: boolean) => Promise<void>; 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; const ONE_MINUTE = 60 * 1000;
export const useUpdateStore = create<UpdateStore>()( export const useUpdateStore = create<UpdateStore>()(
@ -44,7 +30,7 @@ export const useUpdateStore = create<UpdateStore>()(
version: "unknown", version: "unknown",
async getLatestVersion(force = false) { async getLatestVersion(force = false) {
set(() => ({ version: queryMeta("version") ?? "unknown" })); set(() => ({ version: getClientConfig()?.commitId ?? "unknown" }));
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE; const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
if (!force && !overTenMins) return; if (!force && !overTenMins) return;

View File

@ -15,6 +15,27 @@ const nextConfig = {
}; };
if (mode !== "export") { 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 () => { nextConfig.rewrites = async () => {
const ret = [ const ret = [
{ {

View File

@ -8,6 +8,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"export": "BUILD_MODE=export yarn build",
"prompts": "node ./scripts/fetch-prompts.mjs", "prompts": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install", "prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"