Merge pull request #48 from Yidadaa/custom-token

v1.4 Custom Api Key & Copy Code Button
This commit is contained in:
Yifei Zhang 2023-03-26 20:36:45 +08:00 committed by GitHub
commit 84d73fa1f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 128 additions and 28 deletions

View File

@ -2,19 +2,25 @@ import type { ChatRequest } from "../chat/typing";
import { createParser } from "eventsource-parser"; import { createParser } from "eventsource-parser";
import { NextRequest } from "next/server"; import { NextRequest } from "next/server";
const apiKey = process.env.OPENAI_API_KEY; async function createStream(req: NextRequest) {
async function createStream(payload: ReadableStream<Uint8Array>) {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let apiKey = process.env.OPENAI_API_KEY;
const userApiKey = req.headers.get("token");
if (userApiKey) {
apiKey = userApiKey;
console.log("[Stream] using user api key");
}
const res = await fetch("https://api.openai.com/v1/chat/completions", { const res = await fetch("https://api.openai.com/v1/chat/completions", {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
}, },
method: "POST", method: "POST",
body: payload, body: req.body,
}); });
const stream = new ReadableStream({ const stream = new ReadableStream({
@ -49,7 +55,7 @@ async function createStream(payload: ReadableStream<Uint8Array>) {
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const stream = await createStream(req.body!); const stream = await createStream(req);
return new Response(stream); return new Response(stream);
} catch (error) { } catch (error) {
console.error("[Chat Stream]", error); console.error("[Chat Stream]", error);

View File

@ -1,23 +1,26 @@
import { OpenAIApi, Configuration } from "openai"; import { OpenAIApi, Configuration } from "openai";
import { ChatRequest } from "./typing"; import { ChatRequest } from "./typing";
const apiKey = process.env.OPENAI_API_KEY;
const openai = new OpenAIApi(
new Configuration({
apiKey,
})
);
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const requestBody = (await req.json()) as ChatRequest; let apiKey = process.env.OPENAI_API_KEY;
const completion = await openai!.createChatCompletion(
{ const userApiKey = req.headers.get("token");
...requestBody, if (userApiKey) {
} apiKey = userApiKey;
}
const openai = new OpenAIApi(
new Configuration({
apiKey,
})
); );
const requestBody = (await req.json()) as ChatRequest;
const completion = await openai!.createChatCompletion({
...requestBody,
});
return new Response(JSON.stringify(completion.data)); return new Response(JSON.stringify(completion.data));
} catch (e) { } catch (e) {
console.error("[Chat] ", e); console.error("[Chat] ", e);

View File

@ -4,15 +4,36 @@ import RemarkMath from "remark-math";
import RehypeKatex from "rehype-katex"; import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm"; import RemarkGfm from "remark-gfm";
import RehypePrsim from "rehype-prism-plus"; import RehypePrsim from "rehype-prism-plus";
import { useRef } from "react";
import { copyToClipboard } from "../utils";
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
return (
<pre ref={ref}>
<span
className="copy-code-button"
onClick={() => {
if (ref.current) {
const code = ref.current.innerText;
copyToClipboard(code);
}
}}
></span>
{props.children}
</pre>
);
}
export function Markdown(props: { content: string }) { export function Markdown(props: { content: string }) {
return ( return (
<ReactMarkdown <ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm]} remarkPlugins={[RemarkMath, RemarkGfm]}
rehypePlugins={[ rehypePlugins={[RehypeKatex, [RehypePrsim, { ignoreMissing: true }]]}
RehypeKatex, components={{
[RehypePrsim, { ignoreMissing: true }], pre: PreCode,
]} }}
> >
{props.content} {props.content}
</ReactMarkdown> </ReactMarkdown>

View File

@ -257,6 +257,20 @@ export function Settings(props: { closeSettings: () => void }) {
<></> <></>
)} )}
<SettingItem
title={Locale.Settings.Token.Title}
subTitle={Locale.Settings.Token.SubTitle}
>
<input
value={accessStore.token}
type="text"
placeholder={Locale.Settings.Token.Placeholder}
onChange={(e) => {
accessStore.updateToken(e.currentTarget.value);
}}
></input>
</SettingItem>
<SettingItem <SettingItem
title={Locale.Settings.HistoryCount.Title} title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle} subTitle={Locale.Settings.HistoryCount.SubTitle}

View File

@ -69,6 +69,11 @@ const cn = {
Title: "历史消息长度压缩阈值", Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩", SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
}, },
Token: {
Title: "API Key",
SubTitle: "使用自己的 Key 可绕过受控访问限制",
Placeholder: "OpenAI API Key",
},
AccessCode: { AccessCode: {
Title: "访问码", Title: "访问码",
SubTitle: "现在是受控访问状态", SubTitle: "现在是受控访问状态",

View File

@ -74,6 +74,11 @@ const en: LocaleType = {
SubTitle: SubTitle:
"Will compress if uncompressed messages length exceeds the value", "Will compress if uncompressed messages length exceeds the value",
}, },
Token: {
Title: "API Key",
SubTitle: "Use your key to ignore access code limit",
Placeholder: "OpenAI API Key",
},
AccessCode: { AccessCode: {
Title: "Access Code", Title: "Access Code",
SubTitle: "Access control enabled", SubTitle: "Access control enabled",

View File

@ -1,5 +1,5 @@
import { Analytics } from "@vercel/analytics/react"; import { Analytics } from "@vercel/analytics/react";
import { Home } from './components/home' import { Home } from "./components/home";
export default function App() { export default function App() {
return ( return (

View File

@ -35,6 +35,10 @@ function getHeaders() {
headers["access-code"] = accessStore.accessCode; headers["access-code"] = accessStore.accessCode;
} }
if (accessStore.token && accessStore.token.length > 0) {
headers["token"] = accessStore.token;
}
return headers; return headers;
} }

View File

@ -4,7 +4,9 @@ import { queryMeta } from "../utils";
export interface AccessControlStore { export interface AccessControlStore {
accessCode: string; accessCode: string;
token: string;
updateToken: (_: string) => void;
updateCode: (_: string) => void; updateCode: (_: string) => void;
enabledAccessControl: () => boolean; enabledAccessControl: () => boolean;
} }
@ -14,6 +16,7 @@ export const ACCESS_KEY = "access-control";
export const useAccessStore = create<AccessControlStore>()( export const useAccessStore = create<AccessControlStore>()(
persist( persist(
(set, get) => ({ (set, get) => ({
token: "",
accessCode: "", accessCode: "",
enabledAccessControl() { enabledAccessControl() {
return queryMeta("access") === "enabled"; return queryMeta("access") === "enabled";
@ -21,6 +24,9 @@ export const useAccessStore = create<AccessControlStore>()(
updateCode(code: string) { updateCode(code: string) {
set((state) => ({ accessCode: code })); set((state) => ({ accessCode: code }));
}, },
updateToken(token: string) {
set((state) => ({ token }));
},
}), }),
{ {
name: ACCESS_KEY, name: ACCESS_KEY,

View File

@ -49,22 +49,24 @@ export interface ChatConfig {
export type ModelConfig = ChatConfig["modelConfig"]; export type ModelConfig = ChatConfig["modelConfig"];
const ENABLE_GPT4 = true;
export const ALL_MODELS = [ export const ALL_MODELS = [
{ {
name: "gpt-4", name: "gpt-4",
available: false, available: ENABLE_GPT4,
}, },
{ {
name: "gpt-4-0314", name: "gpt-4-0314",
available: false, available: ENABLE_GPT4,
}, },
{ {
name: "gpt-4-32k", name: "gpt-4-32k",
available: false, available: ENABLE_GPT4,
}, },
{ {
name: "gpt-4-32k-0314", name: "gpt-4-32k-0314",
available: false, available: ENABLE_GPT4,
}, },
{ {
name: "gpt-3.5-turbo", name: "gpt-3.5-turbo",

View File

@ -206,3 +206,36 @@ div.math {
text-decoration: underline; text-decoration: underline;
} }
} }
pre {
position: relative;
&:hover .copy-code-button {
pointer-events: all;
transform: translateX(0px);
opacity: 0.5;
}
.copy-code-button {
position: absolute;
right: 10px;
cursor: pointer;
padding: 0px 5px;
background-color: var(--black);
color: var(--white);
border: var(--border-in-light);
border-radius: 10px;
transform: translateX(10px);
pointer-events: none;
opacity: 0;
transition: all ease 0.3s;
&:after {
content: "copy";
}
&:hover {
opacity: 1;
}
}
}

View File

@ -8,13 +8,14 @@ export const config = {
export function middleware(req: NextRequest, res: NextResponse) { export function middleware(req: NextRequest, res: NextResponse) {
const accessCode = req.headers.get("access-code"); const accessCode = req.headers.get("access-code");
const token = req.headers.get("token");
const hashedCode = md5.hash(accessCode ?? "").trim(); const hashedCode = md5.hash(accessCode ?? "").trim();
console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]); console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
console.log("[Auth] got access code:", accessCode); console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode); console.log("[Auth] hashed access code:", hashedCode);
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode)) { if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
return NextResponse.json( return NextResponse.json(
{ {
needAccessCode: true, needAccessCode: true,