diff --git a/README.md b/README.md
index 3973c84bf..a7c862b40 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
ChatGPT Next Web
-English / [简体中文](./README_CN.md) / [日本語](./README_JA.md)
+English / [简体中文](./README_CN.md)
One-Click to get well-designed cross-platform ChatGPT web UI.
@@ -62,6 +62,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
- 🚀 v2.8 now we have a client that runs across all platforms!
+- 🚀 v2.9.11 you can use azure endpoint now.
## 主要功能
@@ -93,6 +94,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
+- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
## Get Started
@@ -153,14 +155,14 @@ After adding or modifying this environment variable, please redeploy the project
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
-### `OPENAI_API_KEY` (required)
-
-Your openai api key.
-
### `CODE` (optional)
Access password, separated by comma.
+### `OPENAI_API_KEY` (required)
+
+Your openai api key.
+
### `BASE_URL` (optional)
> Default: `https://api.openai.com`
@@ -173,6 +175,20 @@ Override openai api request base url.
Specify OpenAI organization ID.
+### `AZURE_URL` (optional)
+
+> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
+
+Azure deploy url.
+
+### `AZURE_API_KEY` (optional)
+
+Azure Api Key.
+
+### `AZURE_API_VERSION` (optional)
+
+Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions).
+
### `HIDE_USER_API_KEY` (optional)
> Default: Empty
diff --git a/README_CN.md b/README_CN.md
index d8e9553e1..c82dfc044 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -90,6 +90,20 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
指定 OpenAI 中的组织 ID。
+### `AZURE_URL` (可选)
+
+> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name}
+
+Azure 部署地址。
+
+### `AZURE_API_KEY` (可选)
+
+Azure 密钥。
+
+### `AZURE_API_VERSION` (可选)
+
+Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
+
### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
diff --git a/app/api/auth.ts b/app/api/auth.ts
index e0453b2b4..c1f6e7fde 100644
--- a/app/api/auth.ts
+++ b/app/api/auth.ts
@@ -28,7 +28,7 @@ export function auth(req: NextRequest) {
const authToken = req.headers.get("Authorization") ?? "";
// check if it is openai api key or user token
- const { accessCode, apiKey: token } = parseApiKey(authToken);
+ const { accessCode, apiKey } = parseApiKey(authToken);
const hashedCode = md5.hash(accessCode ?? "").trim();
@@ -39,7 +39,7 @@ export function auth(req: NextRequest) {
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
- if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
+ if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return {
error: true,
msg: !accessCode ? "empty access code" : "wrong access code",
@@ -47,11 +47,17 @@ export function auth(req: NextRequest) {
}
// if user does not provide an api key, inject system api key
- if (!token) {
- const apiKey = serverConfig.apiKey;
- if (apiKey) {
+ if (!apiKey) {
+ const serverApiKey = serverConfig.isAzure
+ ? serverConfig.azureApiKey
+ : serverConfig.apiKey;
+
+ if (serverApiKey) {
console.log("[Auth] use system api key");
- req.headers.set("Authorization", `Bearer ${apiKey}`);
+ req.headers.set(
+ "Authorization",
+ `${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`,
+ );
} else {
console.log("[Auth] admin did not provide an api key");
}
diff --git a/app/api/common.ts b/app/api/common.ts
index a1decd42f..fc877b02d 100644
--- a/app/api/common.ts
+++ b/app/api/common.ts
@@ -1,19 +1,24 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server";
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
-import { collectModelTable, collectModels } from "../utils/model";
+import { collectModelTable } from "../utils/model";
+import { makeAzurePath } from "../azure";
const serverConfig = getServerSideConfig();
export async function requestOpenai(req: NextRequest) {
const controller = new AbortController();
+
const authValue = req.headers.get("Authorization") ?? "";
- const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
+ const authHeaderName = serverConfig.isAzure ? "api-key" : "Authorization";
+
+ let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/",
"",
);
- let baseUrl = serverConfig.baseUrl ?? OPENAI_BASE_URL;
+ let baseUrl =
+ serverConfig.azureUrl ?? serverConfig.baseUrl ?? OPENAI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
@@ -23,7 +28,7 @@ export async function requestOpenai(req: NextRequest) {
baseUrl = baseUrl.slice(0, -1);
}
- console.log("[Proxy] ", openaiPath);
+ console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
console.log("[Org ID]", serverConfig.openaiOrgId);
@@ -34,14 +39,24 @@ export async function requestOpenai(req: NextRequest) {
10 * 60 * 1000,
);
- const fetchUrl = `${baseUrl}/${openaiPath}`;
+ if (serverConfig.isAzure) {
+ if (!serverConfig.azureApiVersion) {
+ return NextResponse.json({
+ error: true,
+ message: `missing AZURE_API_VERSION in server env vars`,
+ });
+ }
+ path = makeAzurePath(path, serverConfig.azureApiVersion);
+ }
+
+ const fetchUrl = `${baseUrl}/${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
- Authorization: authValue,
- ...(process.env.OPENAI_ORG_ID && {
- "OpenAI-Organization": process.env.OPENAI_ORG_ID,
+ [authHeaderName]: authValue,
+ ...(serverConfig.openaiOrgId && {
+ "OpenAI-Organization": serverConfig.openaiOrgId,
}),
},
method: req.method,
diff --git a/app/azure.ts b/app/azure.ts
new file mode 100644
index 000000000..48406c55b
--- /dev/null
+++ b/app/azure.ts
@@ -0,0 +1,9 @@
+export function makeAzurePath(path: string, apiVersion: string) {
+ // should omit /v1 prefix
+ path = path.replaceAll("v1/", "");
+
+ // should add api-key to query string
+ path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
+
+ return path;
+}
diff --git a/app/client/api.ts b/app/client/api.ts
index b04dd88b8..eedd2c9ab 100644
--- a/app/client/api.ts
+++ b/app/client/api.ts
@@ -1,5 +1,5 @@
import { getClientConfig } from "../config/client";
-import { ACCESS_CODE_PREFIX } from "../constant";
+import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant";
import { ChatMessage, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai";
@@ -127,22 +127,26 @@ export const api = new ClientApi();
export function getHeaders() {
const accessStore = useAccessStore.getState();
- let headers: Record = {
+ const headers: Record = {
"Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest",
};
- const makeBearer = (token: string) => `Bearer ${token.trim()}`;
+ const isAzure = accessStore.provider === ServiceProvider.Azure;
+ const authHeader = isAzure ? "api-key" : "Authorization";
+ const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey;
+
+ const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
const validString = (x: string) => x && x.length > 0;
// use user's api key first
- if (validString(accessStore.token)) {
- headers.Authorization = makeBearer(accessStore.token);
+ if (validString(apiKey)) {
+ headers[authHeader] = makeBearer(apiKey);
} else if (
accessStore.enabledAccessControl() &&
validString(accessStore.accessCode)
) {
- headers.Authorization = makeBearer(
+ headers[authHeader] = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}
diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts
index 97392a004..930d60690 100644
--- a/app/client/platforms/openai.ts
+++ b/app/client/platforms/openai.ts
@@ -1,8 +1,10 @@
import {
+ ApiPath,
DEFAULT_API_HOST,
DEFAULT_MODELS,
OpenaiPath,
REQUEST_TIMEOUT_MS,
+ ServiceProvider,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
@@ -14,6 +16,7 @@ import {
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
+import { makeAzurePath } from "@/app/azure";
export interface OpenAIListModelResponse {
object: string;
@@ -28,20 +31,35 @@ export class ChatGPTApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
- let openaiUrl = useAccessStore.getState().openaiUrl;
- const apiPath = "/api/openai";
+ const accessStore = useAccessStore.getState();
- if (openaiUrl.length === 0) {
+ const isAzure = accessStore.provider === ServiceProvider.Azure;
+
+ if (isAzure && !accessStore.isValidAzure()) {
+ throw Error(
+ "incomplete azure config, please check it in your settings page",
+ );
+ }
+
+ let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
+
+ if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
- openaiUrl = isApp ? DEFAULT_API_HOST : apiPath;
+ baseUrl = isApp ? DEFAULT_API_HOST : ApiPath.OpenAI;
}
- if (openaiUrl.endsWith("/")) {
- openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
+
+ if (baseUrl.endsWith("/")) {
+ baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
- if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) {
- openaiUrl = "https://" + openaiUrl;
+ if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
+ baseUrl = "https://" + baseUrl;
}
- return [openaiUrl, path].join("/");
+
+ if (isAzure) {
+ path = makeAzurePath(path, accessStore.azureApiVersion);
+ }
+
+ return [baseUrl, path].join("/");
}
extractMessage(res: any) {
@@ -70,7 +88,8 @@ export class ChatGPTApi implements LLMApi {
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
- max_tokens: Math.max(modelConfig.max_tokens, 1024),
+ // max_tokens: Math.max(modelConfig.max_tokens, 1024),
+ // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
console.log("[Request] openai payload: ", requestPayload);
@@ -155,14 +174,20 @@ export class ChatGPTApi implements LLMApi {
}
const text = msg.data;
try {
- const json = JSON.parse(text);
- const delta = json.choices[0].delta.content;
+ const json = JSON.parse(text) as {
+ choices: Array<{
+ delta: {
+ content: string;
+ };
+ }>;
+ };
+ const delta = json.choices[0]?.delta?.content;
if (delta) {
responseText += delta;
options.onUpdate?.(responseText, delta);
}
} catch (e) {
- console.error("[Request] parse error", text, msg);
+ console.error("[Request] parse error", text);
}
},
onclose() {
diff --git a/app/components/auth.tsx b/app/components/auth.tsx
index 577d77542..7962d46be 100644
--- a/app/components/auth.tsx
+++ b/app/components/auth.tsx
@@ -18,7 +18,7 @@ export function AuthPage() {
const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => {
accessStore.update((access) => {
- access.token = "";
+ access.openaiApiKey = "";
access.accessCode = "";
});
}; // Reset access code to empty string
@@ -56,11 +56,11 @@ export function AuthPage() {
{
accessStore.update(
- (access) => (access.token = e.currentTarget.value),
+ (access) => (access.openaiApiKey = e.currentTarget.value),
);
}}
/>
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 9afb49f7a..c27c3eee4 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -998,7 +998,9 @@ function _Chat() {
).then((res) => {
if (!res) return;
if (payload.key) {
- accessStore.update((access) => (access.token = payload.key!));
+ accessStore.update(
+ (access) => (access.openaiApiKey = payload.key!),
+ );
}
if (payload.url) {
accessStore.update((access) => (access.openaiUrl = payload.url!));
diff --git a/app/components/settings.tsx b/app/components/settings.tsx
index 572c0743a..178fcec57 100644
--- a/app/components/settings.tsx
+++ b/app/components/settings.tsx
@@ -51,10 +51,13 @@ import Locale, {
import { copyToClipboard } from "../utils";
import Link from "next/link";
import {
+ Azure,
OPENAI_BASE_URL,
Path,
RELEASE_URL,
STORAGE_KEY,
+ ServiceProvider,
+ SlotID,
UPDATE_URL,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
@@ -580,8 +583,16 @@ export function Settings() {
const accessStore = useAccessStore();
const shouldHideBalanceQuery = useMemo(() => {
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
- return accessStore.hideBalanceQuery || isOpenAiUrl;
- }, [accessStore.hideBalanceQuery, accessStore.openaiUrl]);
+ return (
+ accessStore.hideBalanceQuery ||
+ isOpenAiUrl ||
+ accessStore.provider === ServiceProvider.Azure
+ );
+ }, [
+ accessStore.hideBalanceQuery,
+ accessStore.openaiUrl,
+ accessStore.provider,
+ ]);
const usage = {
used: updateStore.used,
@@ -877,16 +888,16 @@ export function Settings() {
-
- {showAccessCode ? (
+
+ {showAccessCode && (
{
accessStore.update(
(access) => (access.accessCode = e.currentTarget.value),
@@ -894,44 +905,152 @@ export function Settings() {
}}
/>
- ) : (
- <>>
)}
- {!accessStore.hideUserApiKey ? (
+ {!accessStore.hideUserApiKey && (
<>
accessStore.update(
- (access) => (access.openaiUrl = e.currentTarget.value),
+ (access) =>
+ (access.useCustomConfig = e.currentTarget.checked),
)
}
>
-
- {
- accessStore.update(
- (access) => (access.token = e.currentTarget.value),
- );
- }}
- />
-
+ {accessStore.useCustomConfig && (
+ <>
+
+
+
+
+ {accessStore.provider === "OpenAI" ? (
+ <>
+
+
+ accessStore.update(
+ (access) =>
+ (access.openaiUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) =>
+ (access.openaiApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ ) : (
+ <>
+
+
+ accessStore.update(
+ (access) =>
+ (access.azureUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) =>
+ (access.azureApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+
+
+ accessStore.update(
+ (access) =>
+ (access.azureApiVersion =
+ e.currentTarget.value),
+ )
+ }
+ >
+
+ >
+ )}
+ >
+ )}
>
- ) : null}
+ )}
{!shouldHideBalanceQuery ? (
- | JSX.Element
- | null
- | undefined;
-}) {
- return {props.children}
;
+export function List(props: { children: React.ReactNode; id?: string }) {
+ return (
+
+ {props.children}
+
+ );
}
export function Loading() {
diff --git a/app/config/server.ts b/app/config/server.ts
index 007c39738..2f2e7d7fd 100644
--- a/app/config/server.ts
+++ b/app/config/server.ts
@@ -4,19 +4,28 @@ import { DEFAULT_MODELS } from "../constant";
declare global {
namespace NodeJS {
interface ProcessEnv {
+ PROXY_URL?: string; // docker only
+
OPENAI_API_KEY?: string;
CODE?: string;
+
BASE_URL?: string;
- PROXY_URL?: string;
- OPENAI_ORG_ID?: string;
+ OPENAI_ORG_ID?: string; // openai only
+
VERCEL?: string;
- HIDE_USER_API_KEY?: string; // disable user's api key input
- DISABLE_GPT4?: string; // allow user to use gpt-4 or not
BUILD_MODE?: "standalone" | "export";
BUILD_APP?: string; // is building desktop app
+
+ HIDE_USER_API_KEY?: string; // disable user's api key input
+ DISABLE_GPT4?: string; // allow user to use gpt-4 or not
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
CUSTOM_MODELS?: string; // to control custom models
+
+ // azure only
+ AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
+ AZURE_API_KEY?: string;
+ AZURE_API_VERSION?: string;
}
}
}
@@ -41,7 +50,7 @@ export const getServerSideConfig = () => {
);
}
- let disableGPT4 = !!process.env.DISABLE_GPT4;
+ const disableGPT4 = !!process.env.DISABLE_GPT4;
let customModels = process.env.CUSTOM_MODELS ?? "";
if (disableGPT4) {
@@ -51,15 +60,25 @@ export const getServerSideConfig = () => {
.join(",");
}
+ const isAzure = !!process.env.AZURE_URL;
+
return {
+ baseUrl: process.env.BASE_URL,
apiKey: process.env.OPENAI_API_KEY,
+ openaiOrgId: process.env.OPENAI_ORG_ID,
+
+ isAzure,
+ azureUrl: process.env.AZURE_URL,
+ azureApiKey: process.env.AZURE_API_KEY,
+ azureApiVersion: process.env.AZURE_API_VERSION,
+
+ needCode: ACCESS_CODES.size > 0,
code: process.env.CODE,
codes: ACCESS_CODES,
- needCode: ACCESS_CODES.size > 0,
- baseUrl: process.env.BASE_URL,
+
proxyUrl: process.env.PROXY_URL,
- openaiOrgId: process.env.OPENAI_ORG_ID,
isVercel: !!process.env.VERCEL,
+
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
disableGPT4,
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
diff --git a/app/constant.ts b/app/constant.ts
index 8d36e0b55..fbc0c72e3 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -23,10 +23,12 @@ export enum Path {
export enum ApiPath {
Cors = "/api/cors",
+ OpenAI = "/api/openai",
}
export enum SlotID {
AppBody = "app-body",
+ CustomModel = "custom-model",
}
export enum FileName {
@@ -60,6 +62,11 @@ export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
+export enum ServiceProvider {
+ OpenAI = "OpenAI",
+ Azure = "Azure",
+}
+
export const OpenaiPath = {
ChatPath: "v1/chat/completions",
UsagePath: "dashboard/billing/usage",
@@ -67,6 +74,10 @@ export const OpenaiPath = {
ListModelPath: "v1/models",
};
+export const Azure = {
+ ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
+};
+
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by OpenAI.
@@ -79,7 +90,6 @@ export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
export const KnowledgeCutOffDate: Record = {
default: "2021-09",
- "gpt-3.5-turbo-1106": "2023-04",
"gpt-4-1106-preview": "2023-04",
"gpt-4-vision-preview": "2023-04",
};
diff --git a/app/locales/ar.ts b/app/locales/ar.ts
index d5844acd6..b58c3a2e8 100644
--- a/app/locales/ar.ts
+++ b/app/locales/ar.ts
@@ -167,11 +167,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Title: "حد الضغط للتاريخ",
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
},
- Token: {
- Title: "مفتاح API",
- SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول",
- Placeholder: "مفتاح OpenAI API",
- },
+
Usage: {
Title: "رصيد الحساب",
SubTitle(used: any, total: any) {
@@ -181,15 +177,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Check: "التحقق",
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
},
- AccessCode: {
- Title: "رمز الوصول",
- SubTitle: "تم تمكين التحكم في الوصول",
- Placeholder: "رمز الوصول المطلوب",
- },
- Endpoint: {
- Title: "نقطة النهاية",
- SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://",
- },
+
Model: "النموذج",
Temperature: {
Title: "الحرارة",
diff --git a/app/locales/bn.ts b/app/locales/bn.ts
index 2db132cec..6dfb0da9b 100644
--- a/app/locales/bn.ts
+++ b/app/locales/bn.ts
@@ -199,11 +199,7 @@ const bn: PartialLocaleType = {
SubTitle:
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
},
- Token: {
- Title: "অ্যাপি কী",
- SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন",
- Placeholder: "OpenAI API কী",
- },
+
Usage: {
Title: "একাউন্ট ব্যালেন্স",
SubTitle(used: any, total: any) {
@@ -213,15 +209,7 @@ const bn: PartialLocaleType = {
Check: "চেক",
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
},
- AccessCode: {
- Title: "অ্যাক্সেস কোড",
- SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়",
- Placeholder: "অ্যাক্সেস কোড প্রয়োজন",
- },
- Endpoint: {
- Title: "ইনটারপয়েন্ট",
- SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে",
- },
+
Model: "মডেল",
Temperature: {
Title: "তাপমাত্রা",
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 4cd963fb8..e721adef7 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -258,11 +258,6 @@ const cn = {
Title: "历史消息长度压缩阈值",
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
},
- Token: {
- Title: "API Key",
- SubTitle: "使用自己的 Key 可绕过密码访问限制",
- Placeholder: "OpenAI API Key",
- },
Usage: {
Title: "余额查询",
@@ -273,19 +268,56 @@ const cn = {
Check: "重新检查",
NoAccess: "输入 API Key 或访问密码查看余额",
},
- AccessCode: {
- Title: "访问密码",
- SubTitle: "管理员已开启加密访问",
- Placeholder: "请输入访问密码",
- },
- Endpoint: {
- Title: "接口地址",
- SubTitle: "除默认地址外,必须包含 http(s)://",
- },
- CustomModel: {
- Title: "自定义模型名",
- SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
+
+ Access: {
+ AccessCode: {
+ Title: "访问密码",
+ SubTitle: "管理员已开启加密访问",
+ Placeholder: "请输入访问密码",
+ },
+ CustomEndpoint: {
+ Title: "自定义接口",
+ SubTitle: "是否使用自定义 Azure 或 OpenAI 服务",
+ },
+ Provider: {
+ Title: "模型服务商",
+ SubTitle: "切换不同的服务商",
+ },
+ OpenAI: {
+ ApiKey: {
+ Title: "API Key",
+ SubTitle: "使用自定义 OpenAI Key 绕过密码访问限制",
+ Placeholder: "OpenAI API Key",
+ },
+
+ Endpoint: {
+ Title: "接口地址",
+ SubTitle: "除默认地址外,必须包含 http(s)://",
+ },
+ },
+ Azure: {
+ ApiKey: {
+ Title: "接口密钥",
+ SubTitle: "使用自定义 Azure Key 绕过密码访问限制",
+ Placeholder: "Azure API Key",
+ },
+
+ Endpoint: {
+ Title: "接口地址",
+ SubTitle: "样例:",
+ },
+
+ ApiVerion: {
+ Title: "接口版本 (azure api version)",
+ SubTitle: "选择指定的部分版本",
+ },
+ },
+ CustomModel: {
+ Title: "自定义模型名",
+ SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
+ },
},
+
Model: "模型 (model)",
Temperature: {
Title: "随机性 (temperature)",
diff --git a/app/locales/cs.ts b/app/locales/cs.ts
index 57aa803e4..c1a84430f 100644
--- a/app/locales/cs.ts
+++ b/app/locales/cs.ts
@@ -124,11 +124,7 @@ const cs: PartialLocaleType = {
SubTitle:
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
},
- Token: {
- Title: "API klíč",
- SubTitle: "Použitím klíče ignorujete omezení přístupového kódu",
- Placeholder: "Klíč API OpenAI",
- },
+
Usage: {
Title: "Stav účtu",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const cs: PartialLocaleType = {
Check: "Zkontrolovat",
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
},
- AccessCode: {
- Title: "Přístupový kód",
- SubTitle: "Kontrola přístupu povolena",
- Placeholder: "Potřebujete přístupový kód",
- },
+
Model: "Model",
Temperature: {
Title: "Teplota",
diff --git a/app/locales/de.ts b/app/locales/de.ts
index e0bdc52b7..2fe871bc9 100644
--- a/app/locales/de.ts
+++ b/app/locales/de.ts
@@ -124,12 +124,7 @@ const de: PartialLocaleType = {
SubTitle:
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
},
- Token: {
- Title: "API-Schlüssel",
- SubTitle:
- "Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
- Placeholder: "OpenAI API-Schlüssel",
- },
+
Usage: {
Title: "Kontostand",
SubTitle(used: any, total: any) {
@@ -139,11 +134,6 @@ const de: PartialLocaleType = {
Check: "Erneut prüfen",
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
},
- AccessCode: {
- Title: "Zugangscode",
- SubTitle: "Zugangskontrolle aktiviert",
- Placeholder: "Zugangscode erforderlich",
- },
Model: "Modell",
Temperature: {
Title: "Temperature", //Temperatur
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 928c4b72d..c6e61ecab 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -262,11 +262,7 @@ const en: LocaleType = {
SubTitle:
"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",
- },
+
Usage: {
Title: "Account Balance",
SubTitle(used: any, total: any) {
@@ -276,19 +272,55 @@ const en: LocaleType = {
Check: "Check",
NoAccess: "Enter API Key to check balance",
},
- AccessCode: {
- Title: "Access Code",
- SubTitle: "Access control enabled",
- Placeholder: "Need Access Code",
- },
- Endpoint: {
- Title: "Endpoint",
- SubTitle: "Custom endpoint must start with http(s)://",
- },
- CustomModel: {
- Title: "Custom Models",
- SubTitle: "Add extra model options, separate by comma",
+ Access: {
+ AccessCode: {
+ Title: "Access Code",
+ SubTitle: "Access control Enabled",
+ Placeholder: "Enter Code",
+ },
+ CustomEndpoint: {
+ Title: "Custom Endpoint",
+ SubTitle: "Use custom Azure or OpenAI service",
+ },
+ Provider: {
+ Title: "Model Provider",
+ SubTitle: "Select Azure or OpenAI",
+ },
+ OpenAI: {
+ ApiKey: {
+ Title: "OpenAI API Key",
+ SubTitle: "User custom OpenAI Api Key",
+ Placeholder: "sk-xxx",
+ },
+
+ Endpoint: {
+ Title: "OpenAI Endpoint",
+ SubTitle: "Must starts with http(s):// or use /api/openai as default",
+ },
+ },
+ Azure: {
+ ApiKey: {
+ Title: "Azure Api Key",
+ SubTitle: "Check your api key from Azure console",
+ Placeholder: "Azure Api Key",
+ },
+
+ Endpoint: {
+ Title: "Azure Endpoint",
+ SubTitle: "Example: ",
+ },
+
+ ApiVerion: {
+ Title: "Azure Api Version",
+ SubTitle: "Check your api version from azure console",
+ },
+ },
+ CustomModel: {
+ Title: "Custom Models",
+ SubTitle: "Custom model options, seperated by comma",
+ },
},
+
Model: "Model",
Temperature: {
Title: "Temperature",
diff --git a/app/locales/es.ts b/app/locales/es.ts
index a6ae154f4..7d742d536 100644
--- a/app/locales/es.ts
+++ b/app/locales/es.ts
@@ -124,11 +124,7 @@ const es: PartialLocaleType = {
SubTitle:
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor",
},
- Token: {
- Title: "Clave de API",
- SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso",
- Placeholder: "Clave de la API de OpenAI",
- },
+
Usage: {
Title: "Saldo de la cuenta",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const es: PartialLocaleType = {
Check: "Comprobar de nuevo",
NoAccess: "Introduzca la clave API para comprobar el saldo",
},
- AccessCode: {
- Title: "Código de acceso",
- SubTitle: "Control de acceso habilitado",
- Placeholder: "Necesita código de acceso",
- },
+
Model: "Modelo",
Temperature: {
Title: "Temperatura",
diff --git a/app/locales/fr.ts b/app/locales/fr.ts
index f5200f271..944754d62 100644
--- a/app/locales/fr.ts
+++ b/app/locales/fr.ts
@@ -173,11 +173,7 @@ const fr: PartialLocaleType = {
SubTitle:
"Comprimera si la longueur des messages non compressés dépasse cette valeur",
},
- Token: {
- Title: "Clé API",
- SubTitle: "Utilisez votre clé pour ignorer la limite du code d'accès",
- Placeholder: "Clé OpenAI API",
- },
+
Usage: {
Title: "Solde du compte",
SubTitle(used: any, total: any) {
@@ -187,11 +183,7 @@ const fr: PartialLocaleType = {
Check: "Vérifier",
NoAccess: "Entrez la clé API pour vérifier le solde",
},
- AccessCode: {
- Title: "Code d'accès",
- SubTitle: "Contrôle d'accès activé",
- Placeholder: "Code d'accès requis",
- },
+
Model: "Modèle",
Temperature: {
Title: "Température",
diff --git a/app/locales/id.ts b/app/locales/id.ts
index b5e4a70b7..4da55948e 100644
--- a/app/locales/id.ts
+++ b/app/locales/id.ts
@@ -4,8 +4,9 @@ import { PartialLocaleType } from "./index";
const id: PartialLocaleType = {
WIP: "Coming Soon...",
Error: {
- Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
- },
+ Unauthorized:
+ "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
+ },
Auth: {
Title: "Diperlukan Kode Akses",
Tips: "Masukkan kode akses di bawah",
@@ -237,11 +238,7 @@ const id: PartialLocaleType = {
SubTitle:
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
},
- Token: {
- Title: "Kunci API",
- SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
- Placeholder: "Kunci API OpenAI",
- },
+
Usage: {
Title: "Saldo Akun",
SubTitle(used: any, total: any) {
@@ -251,15 +248,7 @@ const id: PartialLocaleType = {
Check: "Periksa",
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
},
- AccessCode: {
- Title: "Kode Akses",
- SubTitle: "Kontrol akses diaktifkan",
- Placeholder: "Diperlukan kode akses",
- },
- Endpoint: {
- Title: "Endpoint",
- SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
- },
+
Model: "Model",
Temperature: {
Title: "Suhu",
diff --git a/app/locales/it.ts b/app/locales/it.ts
index bf20747b1..7f0a95846 100644
--- a/app/locales/it.ts
+++ b/app/locales/it.ts
@@ -124,12 +124,7 @@ const it: PartialLocaleType = {
SubTitle:
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
- Token: {
- Title: "API Key",
- SubTitle:
- "Utilizzare la chiave per ignorare il limite del codice di accesso",
- Placeholder: "OpenAI API Key",
- },
+
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
@@ -139,11 +134,7 @@ const it: PartialLocaleType = {
Check: "Controlla ancora",
NoAccess: "Inserire la chiave API per controllare il saldo",
},
- AccessCode: {
- Title: "Codice d'accesso",
- SubTitle: "Controllo d'accesso abilitato",
- Placeholder: "Inserisci il codice d'accesso",
- },
+
Model: "Modello GPT",
Temperature: {
Title: "Temperature",
diff --git a/app/locales/jp.ts b/app/locales/jp.ts
index b63e8ba3a..e0ea07c75 100644
--- a/app/locales/jp.ts
+++ b/app/locales/jp.ts
@@ -20,7 +20,8 @@ const jp: PartialLocaleType = {
Stop: "停止",
Retry: "リトライ",
Pin: "ピン",
- PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました",
+ PinToastContent:
+ "コンテキストプロンプトに1つのメッセージをピン留めしました",
PinToastAction: "表示",
Delete: "削除",
Edit: "編集",
@@ -146,11 +147,7 @@ const jp: PartialLocaleType = {
SubTitle:
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
},
- Token: {
- Title: "APIキー",
- SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する",
- Placeholder: "OpenAI APIキー",
- },
+
Usage: {
Title: "残高照会",
SubTitle(used: any, total: any) {
@@ -160,11 +157,7 @@ const jp: PartialLocaleType = {
Check: "再確認",
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
},
- AccessCode: {
- Title: "アクセスパスワード",
- SubTitle: "暗号化アクセスが有効になっています",
- Placeholder: "アクセスパスワードを入力してください",
- },
+
Model: "モデル (model)",
Temperature: {
Title: "ランダム性 (temperature)",
diff --git a/app/locales/ko.ts b/app/locales/ko.ts
index 717ce30b2..844459fc4 100644
--- a/app/locales/ko.ts
+++ b/app/locales/ko.ts
@@ -124,11 +124,7 @@ const ko: PartialLocaleType = {
Title: "기록 압축 임계값",
SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨",
},
- Token: {
- Title: "API 키",
- SubTitle: "액세스 코드 제한을 무시하기 위해 키 사용",
- Placeholder: "OpenAI API 키",
- },
+
Usage: {
Title: "계정 잔액",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const ko: PartialLocaleType = {
Check: "확인",
NoAccess: "잔액 확인을 위해 API 키를 입력하세요.",
},
- AccessCode: {
- Title: "액세스 코드",
- SubTitle: "액세스 제어가 활성화됨",
- Placeholder: "액세스 코드 입력",
- },
+
Model: "모델",
Temperature: {
Title: "온도 (temperature)",
diff --git a/app/locales/no.ts b/app/locales/no.ts
index 43c92916f..3a0e61107 100644
--- a/app/locales/no.ts
+++ b/app/locales/no.ts
@@ -106,12 +106,7 @@ const no: PartialLocaleType = {
SubTitle:
"Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien",
},
- Token: {
- Title: "API Key",
- SubTitle:
- "Bruk din egen API-nøkkel for å ignorere tilgangskoden begrensning",
- Placeholder: "OpenAI API-nøkkel",
- },
+
Usage: {
Title: "Saldo for konto",
SubTitle(used: any, total: any) {
@@ -121,11 +116,7 @@ const no: PartialLocaleType = {
Check: "Sjekk",
NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo",
},
- AccessCode: {
- Title: "Tilgangskode",
- SubTitle: "Tilgangskontroll på",
- Placeholder: "Trenger tilgangskode",
- },
+
Model: "Model",
Temperature: {
Title: "Temperatur",
diff --git a/app/locales/ru.ts b/app/locales/ru.ts
index bf98b4eb8..d12cf3e42 100644
--- a/app/locales/ru.ts
+++ b/app/locales/ru.ts
@@ -125,11 +125,7 @@ const ru: PartialLocaleType = {
SubTitle:
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
},
- Token: {
- Title: "API ключ",
- SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
- Placeholder: "API ключ OpenAI",
- },
+
Usage: {
Title: "Баланс аккаунта",
SubTitle(used: any, total: any) {
@@ -139,11 +135,7 @@ const ru: PartialLocaleType = {
Check: "Проверить",
NoAccess: "Введите API ключ, чтобы проверить баланс",
},
- AccessCode: {
- Title: "Код доступа",
- SubTitle: "Контроль доступа включен",
- Placeholder: "Требуется код доступа",
- },
+
Model: "Модель",
Temperature: {
Title: "Температура",
diff --git a/app/locales/tr.ts b/app/locales/tr.ts
index 06996d83d..524c1b2c5 100644
--- a/app/locales/tr.ts
+++ b/app/locales/tr.ts
@@ -124,11 +124,7 @@ const tr: PartialLocaleType = {
SubTitle:
"Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır",
},
- Token: {
- Title: "API Anahtarı",
- SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın",
- Placeholder: "OpenAI API Anahtarı",
- },
+
Usage: {
Title: "Hesap Bakiyesi",
SubTitle(used: any, total: any) {
@@ -138,11 +134,7 @@ const tr: PartialLocaleType = {
Check: "Tekrar Kontrol Et",
NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin",
},
- AccessCode: {
- Title: "Erişim Kodu",
- SubTitle: "Erişim kontrolü etkinleştirme",
- Placeholder: "Erişim Kodu Gerekiyor",
- },
+
Model: "Model",
Temperature: {
Title: "Gerçeklik",
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index e9f38d097..af47e30ff 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -120,11 +120,7 @@ const tw: PartialLocaleType = {
Title: "歷史訊息長度壓縮閾值",
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
},
- Token: {
- Title: "API Key",
- SubTitle: "使用自己的 Key 可規避授權存取限制",
- Placeholder: "OpenAI API Key",
- },
+
Usage: {
Title: "帳戶餘額",
SubTitle(used: any, total: any) {
@@ -134,11 +130,7 @@ const tw: PartialLocaleType = {
Check: "重新檢查",
NoAccess: "輸入 API Key 檢視餘額",
},
- AccessCode: {
- Title: "授權碼",
- SubTitle: "目前是未授權存取狀態",
- Placeholder: "請輸入授權碼",
- },
+
Model: "模型 (model)",
Temperature: {
Title: "隨機性 (temperature)",
diff --git a/app/locales/vi.ts b/app/locales/vi.ts
index 8f53a3dc1..3d95b5664 100644
--- a/app/locales/vi.ts
+++ b/app/locales/vi.ts
@@ -123,11 +123,7 @@ const vi: PartialLocaleType = {
Title: "Ngưỡng nén lịch sử tin nhắn",
SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng",
},
- Token: {
- Title: "API Key",
- SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập",
- Placeholder: "OpenAI API Key",
- },
+
Usage: {
Title: "Hạn mức tài khoản",
SubTitle(used: any, total: any) {
@@ -137,11 +133,7 @@ const vi: PartialLocaleType = {
Check: "Kiểm tra",
NoAccess: "Nhập API Key để kiểm tra hạn mức",
},
- AccessCode: {
- Title: "Mã truy cập",
- SubTitle: "Đã bật kiểm soát truy cập",
- Placeholder: "Nhập mã truy cập",
- },
+
Model: "Mô hình",
Temperature: {
Title: "Tính ngẫu nhiên (temperature)",
diff --git a/app/store/access.ts b/app/store/access.ts
index f87e44a2a..2abe1e3cc 100644
--- a/app/store/access.ts
+++ b/app/store/access.ts
@@ -1,25 +1,41 @@
-import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant";
+import {
+ ApiPath,
+ DEFAULT_API_HOST,
+ ServiceProvider,
+ StoreKey,
+} from "../constant";
import { getHeaders } from "../client/api";
import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store";
+import { ensure } from "../utils/clone";
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);
+ getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI;
const DEFAULT_ACCESS_STATE = {
- token: "",
accessCode: "",
+ useCustomConfig: false,
+
+ provider: ServiceProvider.OpenAI,
+
+ // openai
+ openaiUrl: DEFAULT_OPENAI_URL,
+ openaiApiKey: "",
+
+ // azure
+ azureUrl: "",
+ azureApiKey: "",
+ azureApiVersion: "2023-08-01-preview",
+
+ // server config
needCode: true,
hideUserApiKey: false,
hideBalanceQuery: false,
disableGPT4: false,
disableFastLink: false,
customModels: "",
-
- openaiUrl: DEFAULT_OPENAI_URL,
};
export const useAccessStore = createPersistStore(
@@ -31,12 +47,24 @@ export const useAccessStore = createPersistStore(
return get().needCode;
},
+
+ isValidOpenAI() {
+ return ensure(get(), ["openaiUrl", "openaiApiKey"]);
+ },
+
+ isValidAzure() {
+ return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
+ },
+
isAuthorized() {
this.fetch();
// has token or has code or disabled access control
return (
- !!get().token || !!get().accessCode || !this.enabledAccessControl()
+ this.isValidOpenAI() ||
+ this.isValidAzure() ||
+ !this.enabledAccessControl() ||
+ (this.enabledAccessControl() && ensure(get(), ["accessCode"]))
);
},
fetch() {
@@ -64,6 +92,19 @@ export const useAccessStore = createPersistStore(
}),
{
name: StoreKey.Access,
- version: 1,
+ version: 2,
+ migrate(persistedState, version) {
+ if (version < 2) {
+ const state = persistedState as {
+ token: string;
+ openaiApiKey: string;
+ azureApiVersion: string;
+ };
+ state.openaiApiKey = state.token;
+ state.azureApiVersion = "2023-08-01-preview";
+ }
+
+ return persistedState as any;
+ },
},
);
diff --git a/app/store/config.ts b/app/store/config.ts
index 17eb88c30..057e31b25 100644
--- a/app/store/config.ts
+++ b/app/store/config.ts
@@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = {
model: "gpt-3.5-turbo" as ModelType,
temperature: 0.5,
top_p: 1,
- max_tokens: 8192,
+ max_tokens: 4000,
presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true,
diff --git a/app/utils/clone.ts b/app/utils/clone.ts
index 2958b6b9c..c42288f77 100644
--- a/app/utils/clone.ts
+++ b/app/utils/clone.ts
@@ -1,3 +1,10 @@
export function deepClone(obj: T) {
return JSON.parse(JSON.stringify(obj));
}
+
+export function ensure(
+ obj: T,
+ keys: Array<[keyof T][number]>,
+) {
+ return keys.every((k) => obj[k] !== undefined && obj[k] !== null);
+}
diff --git a/app/utils/store.ts b/app/utils/store.ts
index cd151dc49..684a19112 100644
--- a/app/utils/store.ts
+++ b/app/utils/store.ts
@@ -1,5 +1,5 @@
import { create } from "zustand";
-import { persist } from "zustand/middleware";
+import { combine, persist } from "zustand/middleware";
import { Updater } from "../typing";
import { deepClone } from "./clone";
@@ -23,33 +23,42 @@ type SetStoreState = (
replace?: boolean | undefined,
) => void;
-export function createPersistStore(
- defaultState: T,
+export function createPersistStore(
+ state: T,
methods: (
set: SetStoreState>,
get: () => T & MakeUpdater,
) => M,
persistOptions: SecondParam>>,
) {
- return create>()(
- persist((set, get) => {
- return {
- ...defaultState,
- ...methods(set as any, get),
+ return create(
+ persist(
+ combine(
+ {
+ ...state,
+ lastUpdateTime: 0,
+ },
+ (set, get) => {
+ return {
+ ...methods(set, get as any),
- lastUpdateTime: 0,
- markUpdate() {
- set({ lastUpdateTime: Date.now() } as Partial<
- T & M & MakeUpdater
- >);
+ markUpdate() {
+ set({ lastUpdateTime: Date.now() } as Partial<
+ T & M & MakeUpdater
+ >);
+ },
+ update(updater) {
+ const state = deepClone(get());
+ updater(state);
+ set({
+ ...state,
+ lastUpdateTime: Date.now(),
+ });
+ },
+ } as M & MakeUpdater;
},
- update(updater) {
- const state = deepClone(get());
- updater(state);
- get().markUpdate();
- set(state);
- },
- };
- }, persistOptions),
+ ),
+ persistOptions as any,
+ ),
);
}