mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 15:16:24 +08:00
feat: close #2 add check account balance
This commit is contained in:
@@ -1,26 +1,12 @@
|
||||
import { createParser } from "eventsource-parser";
|
||||
import { NextRequest } from "next/server";
|
||||
import { requestOpenai } from "../common";
|
||||
|
||||
async function createStream(req: NextRequest) {
|
||||
const encoder = new TextEncoder();
|
||||
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", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
method: "POST",
|
||||
body: req.body,
|
||||
});
|
||||
const res = await requestOpenai(req);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
|
1
app/api/chat/.gitignore
vendored
1
app/api/chat/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
config.ts
|
@@ -1,29 +0,0 @@
|
||||
import { OpenAIApi, Configuration } from "openai";
|
||||
import { ChatRequest } from "./typing";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
let apiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
const userApiKey = req.headers.get("token");
|
||||
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));
|
||||
} catch (e) {
|
||||
console.error("[Chat] ", e);
|
||||
return new Response(JSON.stringify(e));
|
||||
}
|
||||
}
|
22
app/api/common.ts
Normal file
22
app/api/common.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
const OPENAI_URL = "api.openai.com";
|
||||
const DEFAULT_PROTOCOL = "https";
|
||||
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
|
||||
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
|
||||
|
||||
export async function requestOpenai(req: NextRequest) {
|
||||
const apiKey = req.headers.get("token");
|
||||
const openaiPath = req.headers.get("path");
|
||||
|
||||
console.log("[Proxy] ", openaiPath);
|
||||
|
||||
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
});
|
||||
}
|
28
app/api/openai/route.ts
Normal file
28
app/api/openai/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requestOpenai } from "../common";
|
||||
|
||||
async function makeRequest(req: NextRequest) {
|
||||
try {
|
||||
const res = await requestOpenai(req);
|
||||
return new Response(res.body);
|
||||
} catch (e) {
|
||||
console.error("[OpenAI] ", req.body, e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: JSON.stringify(e),
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return makeRequest(req);
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return makeRequest(req);
|
||||
}
|
@@ -27,6 +27,7 @@ import { getCurrentCommitId } from "../utils";
|
||||
import Link from "next/link";
|
||||
import { UPDATE_URL } from "../constant";
|
||||
import { SearchService, usePromptStore } from "../store/prompt";
|
||||
import { requestUsage } from "../requests";
|
||||
|
||||
function SettingItem(props: {
|
||||
title: string;
|
||||
@@ -54,7 +55,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
state.updateConfig,
|
||||
state.resetConfig,
|
||||
state.clearAllData,
|
||||
]
|
||||
],
|
||||
);
|
||||
|
||||
const updateStore = useUpdateStore();
|
||||
@@ -70,14 +71,34 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
});
|
||||
}
|
||||
|
||||
const [usage, setUsage] = useState<{
|
||||
granted?: number;
|
||||
used?: number;
|
||||
}>();
|
||||
const [loadingUsage, setLoadingUsage] = useState(false);
|
||||
function checkUsage() {
|
||||
setLoadingUsage(true);
|
||||
requestUsage()
|
||||
.then((res) =>
|
||||
setUsage({
|
||||
granted: res?.total_granted,
|
||||
used: res?.total_used,
|
||||
}),
|
||||
)
|
||||
.finally(() => {
|
||||
setLoadingUsage(false);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkUpdate();
|
||||
checkUsage();
|
||||
}, []);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const enabledAccessControl = useMemo(
|
||||
() => accessStore.enabledAccessControl(),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const promptStore = usePromptStore();
|
||||
@@ -179,7 +200,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
onChange={(e) => {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.submitKey = e.target.value as any as SubmitKey)
|
||||
(config.submitKey = e.target.value as any as SubmitKey),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -199,7 +220,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
value={config.theme}
|
||||
onChange={(e) => {
|
||||
updateConfig(
|
||||
(config) => (config.theme = e.target.value as any as Theme)
|
||||
(config) => (config.theme = e.target.value as any as Theme),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -240,7 +261,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.fontSize = Number.parseInt(e.currentTarget.value))
|
||||
(config.fontSize = Number.parseInt(e.currentTarget.value)),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
@@ -253,7 +274,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
checked={config.tightBorder}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.tightBorder = e.currentTarget.checked)
|
||||
(config) => (config.tightBorder = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
@@ -271,7 +292,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.disablePromptHint = e.currentTarget.checked)
|
||||
(config.disablePromptHint = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
@@ -281,7 +302,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
title={Locale.Settings.Prompt.List}
|
||||
subTitle={Locale.Settings.Prompt.ListCount(
|
||||
builtinCount,
|
||||
customCount
|
||||
customCount,
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
@@ -324,6 +345,28 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
></input>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
title={Locale.Settings.Usage.Title}
|
||||
subTitle={
|
||||
loadingUsage
|
||||
? Locale.Settings.Usage.IsChecking
|
||||
: Locale.Settings.Usage.SubTitle(
|
||||
usage?.granted ?? "[?]",
|
||||
usage?.used ?? "[?]",
|
||||
)
|
||||
}
|
||||
>
|
||||
{loadingUsage ? (
|
||||
<div />
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<ResetIcon></ResetIcon>}
|
||||
text={Locale.Settings.Usage.Check}
|
||||
onClick={checkUsage}
|
||||
/>
|
||||
)}
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
title={Locale.Settings.HistoryCount.Title}
|
||||
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
||||
@@ -338,7 +381,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.historyMessageCount = e.target.valueAsNumber)
|
||||
(config.historyMessageCount = e.target.valueAsNumber),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
@@ -357,7 +400,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.compressMessageLengthThreshold =
|
||||
e.currentTarget.valueAsNumber)
|
||||
e.currentTarget.valueAsNumber),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
@@ -370,7 +413,8 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
value={config.modelConfig.model}
|
||||
onChange={(e) => {
|
||||
updateConfig(
|
||||
(config) => (config.modelConfig.model = e.currentTarget.value)
|
||||
(config) =>
|
||||
(config.modelConfig.model = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -395,7 +439,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.modelConfig.temperature =
|
||||
e.currentTarget.valueAsNumber)
|
||||
e.currentTarget.valueAsNumber),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
@@ -413,7 +457,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.modelConfig.max_tokens =
|
||||
e.currentTarget.valueAsNumber)
|
||||
e.currentTarget.valueAsNumber),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
@@ -432,7 +476,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.modelConfig.presence_penalty =
|
||||
e.currentTarget.valueAsNumber)
|
||||
e.currentTarget.valueAsNumber),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
|
@@ -64,6 +64,7 @@ const cn = {
|
||||
Title: "字体大小",
|
||||
SubTitle: "聊天内容的字体大小",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `当前版本:${x}`,
|
||||
IsLatest: "已是最新版本",
|
||||
@@ -98,6 +99,14 @@ const cn = {
|
||||
SubTitle: "使用自己的 Key 可绕过受控访问限制",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
Usage: {
|
||||
Title: "账户余额",
|
||||
SubTitle(granted: any, used: any) {
|
||||
return `总共 $${granted},已使用 $${used}`;
|
||||
},
|
||||
IsChecking: "正在检查…",
|
||||
Check: "重新检查",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "访问码",
|
||||
SubTitle: "现在是受控访问状态",
|
||||
|
@@ -101,6 +101,14 @@ const en: LocaleType = {
|
||||
SubTitle: "Use your key to ignore access code limit",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
Usage: {
|
||||
Title: "Account Balance",
|
||||
SubTitle(granted: any, used: any) {
|
||||
return `Total $${granted}, Used $${used}`;
|
||||
},
|
||||
IsChecking: "Checking...",
|
||||
Check: "Check Again",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Access Code",
|
||||
SubTitle: "Access control enabled",
|
||||
|
@@ -99,6 +99,14 @@ const tw: LocaleType = {
|
||||
SubTitle: "使用自己的 Key 可規避受控訪問限制",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
Usage: {
|
||||
Title: "帳戶餘額",
|
||||
SubTitle(granted: any, used: any) {
|
||||
return `總共 $${granted},已使用 $${used}`;
|
||||
},
|
||||
IsChecking: "正在檢查…",
|
||||
Check: "重新檢查",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "訪問碼",
|
||||
SubTitle: "現在是受控訪問狀態",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { ChatRequest, ChatReponse } from "./api/chat/typing";
|
||||
import type { ChatRequest, ChatReponse } from "./api/openai/typing";
|
||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||
import Locale from "./locales";
|
||||
|
||||
@@ -9,7 +9,7 @@ const makeRequestParam = (
|
||||
options?: {
|
||||
filterBot?: boolean;
|
||||
stream?: boolean;
|
||||
}
|
||||
},
|
||||
): ChatRequest => {
|
||||
let sendMessages = messages.map((v) => ({
|
||||
role: v.role,
|
||||
@@ -42,19 +42,48 @@ function getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function requestOpenaiClient(path: string) {
|
||||
return (body: any, method = "POST") =>
|
||||
fetch("/api/openai", {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
path,
|
||||
...getHeaders(),
|
||||
},
|
||||
body: body && JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestChat(messages: Message[]) {
|
||||
const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
|
||||
|
||||
const res = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
const res = await requestOpenaiClient("v1/chat/completions")(req);
|
||||
|
||||
return (await res.json()) as ChatReponse;
|
||||
try {
|
||||
const response = (await res.json()) as ChatReponse;
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("[Request Chat] ", error, res.body);
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestUsage() {
|
||||
const res = await requestOpenaiClient("dashboard/billing/credit_grants")(
|
||||
null,
|
||||
"GET",
|
||||
);
|
||||
|
||||
try {
|
||||
const response = (await res.json()) as {
|
||||
total_available: number;
|
||||
total_granted: number;
|
||||
total_used: number;
|
||||
};
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("[Request usage] ", error, res.body);
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestChatStream(
|
||||
@@ -65,7 +94,7 @@ export async function requestChatStream(
|
||||
onMessage: (message: string, done: boolean) => void;
|
||||
onError: (error: Error) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const req = makeRequestParam(messages, {
|
||||
stream: true,
|
||||
@@ -87,6 +116,7 @@ export async function requestChatStream(
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
path: "v1/chat/completions",
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
@@ -129,7 +159,7 @@ export async function requestChatStream(
|
||||
responseText = Locale.Error.Unauthorized;
|
||||
finish();
|
||||
} else {
|
||||
console.error("Stream Error");
|
||||
console.error("Stream Error", res.body);
|
||||
options?.onError(new Error("Stream Error"));
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -149,7 +179,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
|
||||
|
||||
const res = await requestChat(messages);
|
||||
|
||||
return res.choices.at(0)?.message?.content ?? "";
|
||||
return res?.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
// To store message streaming controller
|
||||
@@ -159,7 +189,7 @@ export const ControllerPool = {
|
||||
addController(
|
||||
sessionIndex: number,
|
||||
messageIndex: number,
|
||||
controller: AbortController
|
||||
controller: AbortController,
|
||||
) {
|
||||
const key = this.key(sessionIndex, messageIndex);
|
||||
this.controllers[key] = controller;
|
||||
|
Reference in New Issue
Block a user