mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 16:51:54 +08:00
16
app/api/access.ts
Normal file
16
app/api/access.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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();
|
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
|
||||
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Theme,
|
||||
ALL_MODELS,
|
||||
useUpdateStore,
|
||||
useAccessStore,
|
||||
} from "../store";
|
||||
import { Avatar } from "./home";
|
||||
|
||||
@@ -38,7 +39,7 @@ function SettingItem(props: {
|
||||
<div className={styles["settings-sub-title"]}>{props.subTitle}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>{props.children}</div>
|
||||
{props.children}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
@@ -71,6 +72,12 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
checkUpdate();
|
||||
}, []);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const enabledAccessControl = useMemo(
|
||||
() => accessStore.enabledAccessControl(),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles["window-header"]}>
|
||||
@@ -232,6 +239,20 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
</div>
|
||||
</List>
|
||||
<List>
|
||||
<SettingItem
|
||||
title={Locale.Settings.AccessCode.Title}
|
||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
>
|
||||
<input
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.AccessCode.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateCode(e.currentTarget.value);
|
||||
}}
|
||||
></input>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
title={Locale.Settings.HistoryCount.Title}
|
||||
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
||||
|
@@ -3,16 +3,32 @@ import "./styles/globals.scss";
|
||||
import "./styles/markdown.scss";
|
||||
import "./styles/prism.scss";
|
||||
import process from "child_process";
|
||||
import { ACCESS_CODES } from "./api/access";
|
||||
|
||||
const COMMIT_ID = process
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
export const metadata = {
|
||||
title: "ChatGPT Next Web",
|
||||
description: "Your personal ChatGPT Chat Bot.",
|
||||
};
|
||||
|
||||
const COMMIT_ID = process
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
function Meta() {
|
||||
const metas = {
|
||||
version: COMMIT_ID,
|
||||
access: ACCESS_CODES.size > 0 ? "enabled" : "disabled",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(metas).map(([k, v]) => (
|
||||
<meta name={k} content={v} key={k} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -26,7 +42,7 @@ export default function RootLayout({
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<meta name="version" content={COMMIT_ID} />
|
||||
<Meta />
|
||||
<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>
|
||||
|
@@ -1,5 +1,8 @@
|
||||
const cn = {
|
||||
WIP: "该功能仍在开发中……",
|
||||
Error: {
|
||||
Unauthorized: "现在是未授权状态,请在设置页填写授权码。",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 条对话`,
|
||||
},
|
||||
@@ -65,6 +68,11 @@ const cn = {
|
||||
Title: "历史消息长度压缩阈值",
|
||||
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "访问码",
|
||||
SubTitle: "现在是受控访问状态",
|
||||
Placeholder: "请输入访问码",
|
||||
},
|
||||
Model: "模型 (model)",
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
|
@@ -2,6 +2,10 @@ import type { LocaleType } from "./index";
|
||||
|
||||
const en: LocaleType = {
|
||||
WIP: "WIP...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Unauthorized access, please enter access code in settings page.",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} messages`,
|
||||
},
|
||||
@@ -69,6 +73,11 @@ const en: LocaleType = {
|
||||
SubTitle:
|
||||
"Will compress if uncompressed messages length exceeds the value",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Access Code",
|
||||
SubTitle: "Access control enabled",
|
||||
Placeholder: "Need Access Code",
|
||||
},
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { ChatRequest, ChatReponse } from "./api/chat/typing";
|
||||
import { filterConfig, Message, ModelConfig } from "./store";
|
||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||
import Locale from "./locales";
|
||||
|
||||
const TIME_OUT_MS = 30000;
|
||||
|
||||
@@ -26,6 +27,17 @@ const makeRequestParam = (
|
||||
};
|
||||
};
|
||||
|
||||
function getHeaders() {
|
||||
const accessStore = useAccessStore.getState();
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (accessStore.enabledAccessControl()) {
|
||||
headers["access-code"] = accessStore.accessCode;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function requestChat(messages: Message[]) {
|
||||
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
|
||||
|
||||
@@ -33,6 +45,7 @@ export async function requestChat(messages: Message[]) {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
@@ -69,6 +82,7 @@ export async function requestChatStream(
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
signal: controller.signal,
|
||||
@@ -82,6 +96,8 @@ export async function requestChatStream(
|
||||
controller.abort();
|
||||
};
|
||||
|
||||
console.log(res);
|
||||
|
||||
if (res.ok) {
|
||||
const reader = res.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
@@ -102,14 +118,18 @@ export async function requestChatStream(
|
||||
}
|
||||
}
|
||||
|
||||
finish();
|
||||
} else if (res.status === 401) {
|
||||
console.error("Anauthorized");
|
||||
responseText = Locale.Error.Unauthorized;
|
||||
finish();
|
||||
} else {
|
||||
console.error("Stream Error");
|
||||
options?.onError(new Error("Stream Error"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("NetWork Error");
|
||||
options?.onError(new Error("NetWork Error"));
|
||||
console.error("NetWork Error", err);
|
||||
options?.onError(err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
30
app/store/access.ts
Normal file
30
app/store/access.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { queryMeta } from "../utils";
|
||||
|
||||
export interface AccessControlStore {
|
||||
accessCode: string;
|
||||
|
||||
updateCode: (_: string) => void;
|
||||
enabledAccessControl: () => boolean;
|
||||
}
|
||||
|
||||
export const ACCESS_KEY = "access-control";
|
||||
|
||||
export const useAccessStore = create<AccessControlStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
accessCode: "",
|
||||
enabledAccessControl() {
|
||||
return queryMeta("access") === "enabled";
|
||||
},
|
||||
updateCode(code: string) {
|
||||
set((state) => ({ accessCode: code }));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: ACCESS_KEY,
|
||||
version: 1,
|
||||
}
|
||||
)
|
||||
);
|
@@ -308,6 +308,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
onMessage(content, done) {
|
||||
if (done) {
|
||||
botMessage.streaming = false;
|
||||
botMessage.content = content;
|
||||
get().onNewMessage(botMessage);
|
||||
} else {
|
||||
botMessage.content = content;
|
||||
|
@@ -1,2 +1,3 @@
|
||||
export * from "./app";
|
||||
export * from "./update";
|
||||
export * from "./access";
|
||||
|
@@ -167,7 +167,8 @@ input[type="range"]::-webkit-slider-thumb:hover {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
input[type="number"],
|
||||
input[type="text"] {
|
||||
appearance: none;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
@@ -176,6 +177,7 @@ input[type="number"] {
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
padding: 0 10px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
div.math {
|
||||
|
23
app/utils.ts
23
app/utils.ts
@@ -57,20 +57,27 @@ 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 getCurrentCommitId() {
|
||||
if (currentId) {
|
||||
return currentId;
|
||||
}
|
||||
|
||||
if (document) {
|
||||
const meta = document.head.querySelector(
|
||||
"meta[name='version']"
|
||||
) as HTMLMetaElement;
|
||||
currentId = meta?.content ?? "";
|
||||
} else {
|
||||
currentId = process.env.COMMIT_ID ?? "";
|
||||
}
|
||||
currentId = queryMeta("version");
|
||||
|
||||
return currentId;
|
||||
}
|
||||
|
Reference in New Issue
Block a user