mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-01 20:56:59 +08:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2bfb362832 | ||
|
cb140e482f | ||
|
e6b72ac1ff | ||
|
8032e6d68d | ||
|
c7e0a6f37f | ||
|
1141cd2e6e | ||
|
c9dd953817 | ||
|
b7ffca031e | ||
|
fd2f441e02 | ||
|
3b3ebda34b | ||
|
87e3d663a2 | ||
|
d0a1d910d4 | ||
|
33b97082fa | ||
|
d93f05f511 | ||
|
3dc29197e1 | ||
|
fbc0236748 | ||
|
1304a3943a | ||
|
8c0ba1aee2 | ||
|
47154773f2 | ||
|
deca9bf06d | ||
|
6ab4c9be2e | ||
|
3a519612a0 | ||
|
d1ec26ae83 | ||
|
9cb889c34f |
@@ -17,11 +17,6 @@ BASE_URL=
|
|||||||
# Default: Empty
|
# Default: Empty
|
||||||
OPENAI_ORG_ID=
|
OPENAI_ORG_ID=
|
||||||
|
|
||||||
# (optional)
|
|
||||||
# Default: Empty
|
|
||||||
# If you do not want users to input their own API key, set this value to 1.
|
|
||||||
HIDE_USER_API_KEY=
|
|
||||||
|
|
||||||
# (optional)
|
# (optional)
|
||||||
# Default: Empty
|
# Default: Empty
|
||||||
# If you do not want users to use GPT-4, set this value to 1.
|
# If you do not want users to use GPT-4, set this value to 1.
|
||||||
@@ -29,5 +24,15 @@ DISABLE_GPT4=
|
|||||||
|
|
||||||
# (optional)
|
# (optional)
|
||||||
# Default: Empty
|
# Default: Empty
|
||||||
# If you do not want users to query balance, set this value to 1.
|
# If you do not want users to input their own API key, set this value to 1.
|
||||||
HIDE_BALANCE_QUERY=
|
HIDE_USER_API_KEY=
|
||||||
|
|
||||||
|
# (optional)
|
||||||
|
# Default: Empty
|
||||||
|
# If you do want users to query balance, set this value to 1.
|
||||||
|
ENABLE_BALANCE_QUERY=
|
||||||
|
|
||||||
|
# (optional)
|
||||||
|
# Default: Empty
|
||||||
|
# If you want to disable parse settings from url, set this value to 1.
|
||||||
|
DISABLE_FAST_LINK=
|
||||||
|
33
README.md
33
README.md
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<h1 align="center">ChatGPT Next Web</h1>
|
<h1 align="center">ChatGPT Next Web</h1>
|
||||||
|
|
||||||
English / [简体中文](./README_CN.md) / [日本語](./README_JA.md)
|
English / [简体中文](./README_CN.md)
|
||||||
|
|
||||||
One-Click to get well-designed cross-platform ChatGPT web UI.
|
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.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.7 let's share conversations as image, or share to ShareGPT!
|
||||||
- 🚀 v2.8 now we have a client that runs across all platforms!
|
- 🚀 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
|
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
|
||||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
|
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
|
||||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
|
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
|
||||||
|
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
@@ -153,14 +155,14 @@ After adding or modifying this environment variable, please redeploy the project
|
|||||||
|
|
||||||
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
|
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
|
||||||
|
|
||||||
### `OPENAI_API_KEY` (required)
|
|
||||||
|
|
||||||
Your openai api key.
|
|
||||||
|
|
||||||
### `CODE` (optional)
|
### `CODE` (optional)
|
||||||
|
|
||||||
Access password, separated by comma.
|
Access password, separated by comma.
|
||||||
|
|
||||||
|
### `OPENAI_API_KEY` (required)
|
||||||
|
|
||||||
|
Your openai api key.
|
||||||
|
|
||||||
### `BASE_URL` (optional)
|
### `BASE_URL` (optional)
|
||||||
|
|
||||||
> Default: `https://api.openai.com`
|
> Default: `https://api.openai.com`
|
||||||
@@ -173,6 +175,20 @@ Override openai api request base url.
|
|||||||
|
|
||||||
Specify OpenAI organization ID.
|
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)
|
### `HIDE_USER_API_KEY` (optional)
|
||||||
|
|
||||||
> Default: Empty
|
> Default: Empty
|
||||||
@@ -197,6 +213,13 @@ If you do want users to query balance, set this value to 1, or you should set it
|
|||||||
|
|
||||||
If you want to disable parse settings from url, set this to 1.
|
If you want to disable parse settings from url, set this to 1.
|
||||||
|
|
||||||
|
### `CUSTOM_MODELS` (optional)
|
||||||
|
|
||||||
|
> Default: Empty
|
||||||
|
> Example: `+llama,+claude-2,-gpt-3.5-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list.
|
||||||
|
|
||||||
|
To control custom models, use `+` to add a custom model, use `-` to hide a model, separated by comma.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
NodeJS >= 18, Docker >= 20
|
NodeJS >= 18, Docker >= 20
|
||||||
|
22
README_CN.md
22
README_CN.md
@@ -90,6 +90,20 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
|||||||
|
|
||||||
指定 OpenAI 中的组织 ID。
|
指定 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` (可选)
|
### `HIDE_USER_API_KEY` (可选)
|
||||||
|
|
||||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
||||||
@@ -106,6 +120,12 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
|||||||
|
|
||||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
|
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
|
||||||
|
|
||||||
|
### `CUSTOM_MODELS` (可选)
|
||||||
|
|
||||||
|
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`。
|
||||||
|
|
||||||
|
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,用英文逗号隔开。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
点击下方按钮,开始二次开发:
|
点击下方按钮,开始二次开发:
|
||||||
@@ -118,7 +138,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
|||||||
OPENAI_API_KEY=<your api key here>
|
OPENAI_API_KEY=<your api key here>
|
||||||
|
|
||||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||||
BASE_URL=https://nb.nextweb.fun/api/proxy
|
BASE_URL=https://ab.nextweb.fun/api/proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
### 本地开发
|
### 本地开发
|
||||||
|
@@ -28,7 +28,7 @@ export function auth(req: NextRequest) {
|
|||||||
const authToken = req.headers.get("Authorization") ?? "";
|
const authToken = req.headers.get("Authorization") ?? "";
|
||||||
|
|
||||||
// check if it is openai api key or user token
|
// 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();
|
const hashedCode = md5.hash(accessCode ?? "").trim();
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export function auth(req: NextRequest) {
|
|||||||
console.log("[User IP] ", getIP(req));
|
console.log("[User IP] ", getIP(req));
|
||||||
console.log("[Time] ", new Date().toLocaleString());
|
console.log("[Time] ", new Date().toLocaleString());
|
||||||
|
|
||||||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
|
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
msg: !accessCode ? "empty access code" : "wrong access code",
|
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 user does not provide an api key, inject system api key
|
||||||
if (!token) {
|
if (!apiKey) {
|
||||||
const apiKey = serverConfig.apiKey;
|
const serverApiKey = serverConfig.isAzure
|
||||||
if (apiKey) {
|
? serverConfig.azureApiKey
|
||||||
|
: serverConfig.apiKey;
|
||||||
|
|
||||||
|
if (serverApiKey) {
|
||||||
console.log("[Auth] use system api key");
|
console.log("[Auth] use system api key");
|
||||||
req.headers.set("Authorization", `Bearer ${apiKey}`);
|
req.headers.set(
|
||||||
|
"Authorization",
|
||||||
|
`${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("[Auth] admin did not provide an api key");
|
console.log("[Auth] admin did not provide an api key");
|
||||||
}
|
}
|
||||||
|
@@ -1,48 +1,62 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSideConfig } from "../config/server";
|
||||||
|
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
|
||||||
|
import { collectModelTable } from "../utils/model";
|
||||||
|
import { makeAzurePath } from "../azure";
|
||||||
|
|
||||||
export const OPENAI_URL = "api.openai.com";
|
const serverConfig = getServerSideConfig();
|
||||||
const DEFAULT_PROTOCOL = "https";
|
|
||||||
const PROTOCOL = process.env.PROTOCOL || DEFAULT_PROTOCOL;
|
|
||||||
const BASE_URL = process.env.BASE_URL || OPENAI_URL;
|
|
||||||
const DISABLE_GPT4 = !!process.env.DISABLE_GPT4;
|
|
||||||
|
|
||||||
export async function requestOpenai(req: NextRequest) {
|
export async function requestOpenai(req: NextRequest) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const authValue = req.headers.get("Authorization") ?? "";
|
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/",
|
"/api/openai/",
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
|
||||||
let baseUrl = BASE_URL;
|
let baseUrl =
|
||||||
|
serverConfig.azureUrl ?? serverConfig.baseUrl ?? OPENAI_BASE_URL;
|
||||||
|
|
||||||
if (!baseUrl.startsWith("http")) {
|
if (!baseUrl.startsWith("http")) {
|
||||||
baseUrl = `${PROTOCOL}://${baseUrl}`;
|
baseUrl = `https://${baseUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseUrl.endsWith('/')) {
|
if (baseUrl.endsWith("/")) {
|
||||||
baseUrl = baseUrl.slice(0, -1);
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Proxy] ", openaiPath);
|
console.log("[Proxy] ", path);
|
||||||
console.log("[Base Url]", baseUrl);
|
console.log("[Base Url]", baseUrl);
|
||||||
|
console.log("[Org ID]", serverConfig.openaiOrgId);
|
||||||
|
|
||||||
if (process.env.OPENAI_ORG_ID) {
|
const timeoutId = setTimeout(
|
||||||
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 timeoutId = setTimeout(() => {
|
const fetchUrl = `${baseUrl}/${path}`;
|
||||||
controller.abort();
|
|
||||||
}, 10 * 60 * 1000);
|
|
||||||
|
|
||||||
const fetchUrl = `${baseUrl}/${openaiPath}`;
|
|
||||||
const fetchOptions: RequestInit = {
|
const fetchOptions: RequestInit = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
Authorization: authValue,
|
[authHeaderName]: authValue,
|
||||||
...(process.env.OPENAI_ORG_ID && {
|
...(serverConfig.openaiOrgId && {
|
||||||
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
|
"OpenAI-Organization": serverConfig.openaiOrgId,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
method: req.method,
|
method: req.method,
|
||||||
@@ -55,18 +69,23 @@ export async function requestOpenai(req: NextRequest) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// #1815 try to refuse gpt4 request
|
// #1815 try to refuse gpt4 request
|
||||||
if (DISABLE_GPT4 && req.body) {
|
if (serverConfig.customModels && req.body) {
|
||||||
try {
|
try {
|
||||||
|
const modelTable = collectModelTable(
|
||||||
|
DEFAULT_MODELS,
|
||||||
|
serverConfig.customModels,
|
||||||
|
);
|
||||||
const clonedBody = await req.text();
|
const clonedBody = await req.text();
|
||||||
fetchOptions.body = clonedBody;
|
fetchOptions.body = clonedBody;
|
||||||
|
|
||||||
const jsonBody = JSON.parse(clonedBody);
|
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||||
|
|
||||||
if ((jsonBody?.model ?? "").includes("gpt-4")) {
|
// not undefined and is false
|
||||||
|
if (modelTable[jsonBody?.model ?? ""] === false) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
message: "you are not allowed to use gpt-4 model",
|
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 403,
|
status: 403,
|
||||||
|
@@ -12,6 +12,7 @@ const DANGER_CONFIG = {
|
|||||||
disableGPT4: serverConfig.disableGPT4,
|
disableGPT4: serverConfig.disableGPT4,
|
||||||
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
||||||
disableFastLink: serverConfig.disableFastLink,
|
disableFastLink: serverConfig.disableFastLink,
|
||||||
|
customModels: serverConfig.customModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
9
app/azure.ts
Normal file
9
app/azure.ts
Normal file
@@ -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;
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import { getClientConfig } from "../config/client";
|
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 { ChatMessage, ModelType, useAccessStore } from "../store";
|
||||||
import { ChatGPTApi } from "./platforms/openai";
|
import { ChatGPTApi } from "./platforms/openai";
|
||||||
|
|
||||||
@@ -127,22 +127,26 @@ export const api = new ClientApi();
|
|||||||
|
|
||||||
export function getHeaders() {
|
export function getHeaders() {
|
||||||
const accessStore = useAccessStore.getState();
|
const accessStore = useAccessStore.getState();
|
||||||
let headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-requested-with": "XMLHttpRequest",
|
"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;
|
const validString = (x: string) => x && x.length > 0;
|
||||||
|
|
||||||
// use user's api key first
|
// use user's api key first
|
||||||
if (validString(accessStore.token)) {
|
if (validString(apiKey)) {
|
||||||
headers.Authorization = makeBearer(accessStore.token);
|
headers[authHeader] = makeBearer(apiKey);
|
||||||
} else if (
|
} else if (
|
||||||
accessStore.enabledAccessControl() &&
|
accessStore.enabledAccessControl() &&
|
||||||
validString(accessStore.accessCode)
|
validString(accessStore.accessCode)
|
||||||
) {
|
) {
|
||||||
headers.Authorization = makeBearer(
|
headers[authHeader] = makeBearer(
|
||||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
ApiPath,
|
||||||
DEFAULT_API_HOST,
|
DEFAULT_API_HOST,
|
||||||
DEFAULT_MODELS,
|
DEFAULT_MODELS,
|
||||||
OpenaiPath,
|
OpenaiPath,
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
|
ServiceProvider,
|
||||||
} from "@/app/constant";
|
} from "@/app/constant";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ import {
|
|||||||
} from "@fortaine/fetch-event-source";
|
} from "@fortaine/fetch-event-source";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { makeAzurePath } from "@/app/azure";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
@@ -28,20 +31,35 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
private disableListModels = true;
|
private disableListModels = true;
|
||||||
|
|
||||||
path(path: string): string {
|
path(path: string): string {
|
||||||
let openaiUrl = useAccessStore.getState().openaiUrl;
|
const accessStore = useAccessStore.getState();
|
||||||
const apiPath = "/api/openai";
|
|
||||||
|
|
||||||
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;
|
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)) {
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
|
||||||
openaiUrl = "https://" + openaiUrl;
|
baseUrl = "https://" + baseUrl;
|
||||||
}
|
}
|
||||||
return [openaiUrl, path].join("/");
|
|
||||||
|
if (isAzure) {
|
||||||
|
path = makeAzurePath(path, accessStore.azureApiVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [baseUrl, path].join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
extractMessage(res: any) {
|
extractMessage(res: any) {
|
||||||
@@ -70,6 +88,8 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
presence_penalty: modelConfig.presence_penalty,
|
presence_penalty: modelConfig.presence_penalty,
|
||||||
frequency_penalty: modelConfig.frequency_penalty,
|
frequency_penalty: modelConfig.frequency_penalty,
|
||||||
top_p: modelConfig.top_p,
|
top_p: modelConfig.top_p,
|
||||||
|
// 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);
|
console.log("[Request] openai payload: ", requestPayload);
|
||||||
@@ -154,14 +174,20 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
}
|
}
|
||||||
const text = msg.data;
|
const text = msg.data;
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text) as {
|
||||||
const delta = json.choices[0].delta.content;
|
choices: Array<{
|
||||||
|
delta: {
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
const delta = json.choices[0]?.delta?.content;
|
||||||
if (delta) {
|
if (delta) {
|
||||||
responseText += delta;
|
responseText += delta;
|
||||||
options.onUpdate?.(responseText, delta);
|
options.onUpdate?.(responseText, delta);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Request] parse error", text, msg);
|
console.error("[Request] parse error", text);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onclose() {
|
onclose() {
|
||||||
|
@@ -18,7 +18,7 @@ export function AuthPage() {
|
|||||||
const goChat = () => navigate(Path.Chat);
|
const goChat = () => navigate(Path.Chat);
|
||||||
const resetAccessCode = () => {
|
const resetAccessCode = () => {
|
||||||
accessStore.update((access) => {
|
accessStore.update((access) => {
|
||||||
access.token = "";
|
access.openaiApiKey = "";
|
||||||
access.accessCode = "";
|
access.accessCode = "";
|
||||||
});
|
});
|
||||||
}; // Reset access code to empty string
|
}; // Reset access code to empty string
|
||||||
@@ -56,11 +56,11 @@ export function AuthPage() {
|
|||||||
<input
|
<input
|
||||||
className={styles["auth-input"]}
|
className={styles["auth-input"]}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={Locale.Settings.Token.Placeholder}
|
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||||
value={accessStore.token}
|
value={accessStore.openaiApiKey}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.token = e.currentTarget.value),
|
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@@ -18,6 +18,7 @@ import { MaskAvatar } from "./mask";
|
|||||||
import { Mask } from "../store/mask";
|
import { Mask } from "../store/mask";
|
||||||
import { useRef, useEffect } from "react";
|
import { useRef, useEffect } from "react";
|
||||||
import { showConfirm } from "./ui-lib";
|
import { showConfirm } from "./ui-lib";
|
||||||
|
import { useMobileScreen } from "../utils";
|
||||||
|
|
||||||
export function ChatItem(props: {
|
export function ChatItem(props: {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@@ -80,7 +81,11 @@ export function ChatItem(props: {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles["chat-item-delete"]}
|
className={styles["chat-item-delete"]}
|
||||||
onClickCapture={props.onDelete}
|
onClickCapture={(e) => {
|
||||||
|
props.onDelete?.();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</div>
|
</div>
|
||||||
@@ -101,6 +106,7 @@ export function ChatList(props: { narrow?: boolean }) {
|
|||||||
);
|
);
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = (result) => {
|
const onDragEnd: OnDragEndResponder = (result) => {
|
||||||
const { destination, source } = result;
|
const { destination, source } = result;
|
||||||
@@ -142,7 +148,7 @@ export function ChatList(props: { narrow?: boolean }) {
|
|||||||
}}
|
}}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
if (
|
if (
|
||||||
!props.narrow ||
|
(!props.narrow && !isMobileScreen) ||
|
||||||
(await showConfirm(Locale.Home.DeleteChat))
|
(await showConfirm(Locale.Home.DeleteChat))
|
||||||
) {
|
) {
|
||||||
chatStore.deleteSession(i);
|
chatStore.deleteSession(i);
|
||||||
|
@@ -88,6 +88,7 @@ import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
|
|||||||
import { prettyObject } from "../utils/format";
|
import { prettyObject } from "../utils/format";
|
||||||
import { ExportMessageModal } from "./exporter";
|
import { ExportMessageModal } from "./exporter";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
|
import { useAllModels } from "../utils/hooks";
|
||||||
|
|
||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
loading: () => <LoadingIcon />,
|
loading: () => <LoadingIcon />,
|
||||||
@@ -143,6 +144,7 @@ export function SessionConfigModel(props: { onClose: () => void }) {
|
|||||||
extraListItems={
|
extraListItems={
|
||||||
session.mask.modelConfig.sendMemory ? (
|
session.mask.modelConfig.sendMemory ? (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
className="copyable"
|
||||||
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
|
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
|
||||||
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
|
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
|
||||||
></ListItem>
|
></ListItem>
|
||||||
@@ -429,14 +431,9 @@ export function ChatActions(props: {
|
|||||||
|
|
||||||
// switch model
|
// switch model
|
||||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||||
const models = useMemo(
|
const models = useAllModels()
|
||||||
() =>
|
.filter((m) => m.available)
|
||||||
config
|
.map((m) => m.name);
|
||||||
.allModels()
|
|
||||||
.filter((m) => m.available)
|
|
||||||
.map((m) => m.name),
|
|
||||||
[config],
|
|
||||||
);
|
|
||||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1001,7 +998,9 @@ function _Chat() {
|
|||||||
).then((res) => {
|
).then((res) => {
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
if (payload.key) {
|
if (payload.key) {
|
||||||
accessStore.update((access) => (access.token = payload.key!));
|
accessStore.update(
|
||||||
|
(access) => (access.openaiApiKey = payload.key!),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (payload.url) {
|
if (payload.url) {
|
||||||
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { ModalConfigValidator, ModelConfig, useAppConfig } from "../store";
|
import { ModalConfigValidator, ModelConfig } from "../store";
|
||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { InputRange } from "./input-range";
|
import { InputRange } from "./input-range";
|
||||||
import { ListItem, Select } from "./ui-lib";
|
import { ListItem, Select } from "./ui-lib";
|
||||||
|
import { useAllModels } from "../utils/hooks";
|
||||||
|
|
||||||
export function ModelConfigList(props: {
|
export function ModelConfigList(props: {
|
||||||
modelConfig: ModelConfig;
|
modelConfig: ModelConfig;
|
||||||
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
||||||
}) {
|
}) {
|
||||||
const config = useAppConfig();
|
const allModels = useAllModels();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -24,7 +25,7 @@ export function ModelConfigList(props: {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config.allModels().map((v, i) => (
|
{allModels.map((v, i) => (
|
||||||
<option value={v.name} key={i} disabled={!v.available}>
|
<option value={v.name} key={i} disabled={!v.available}>
|
||||||
{v.name}
|
{v.name}
|
||||||
</option>
|
</option>
|
||||||
@@ -75,8 +76,8 @@ export function ModelConfigList(props: {
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={100}
|
min={1024}
|
||||||
max={100000}
|
max={512000}
|
||||||
value={props.modelConfig.max_tokens}
|
value={props.modelConfig.max_tokens}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
props.updateConfig(
|
props.updateConfig(
|
||||||
|
@@ -51,10 +51,13 @@ import Locale, {
|
|||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
|
Azure,
|
||||||
OPENAI_BASE_URL,
|
OPENAI_BASE_URL,
|
||||||
Path,
|
Path,
|
||||||
RELEASE_URL,
|
RELEASE_URL,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
|
ServiceProvider,
|
||||||
|
SlotID,
|
||||||
UPDATE_URL,
|
UPDATE_URL,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
@@ -580,8 +583,16 @@ export function Settings() {
|
|||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const shouldHideBalanceQuery = useMemo(() => {
|
const shouldHideBalanceQuery = useMemo(() => {
|
||||||
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
|
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
|
||||||
return accessStore.hideBalanceQuery || isOpenAiUrl;
|
return (
|
||||||
}, [accessStore.hideBalanceQuery, accessStore.openaiUrl]);
|
accessStore.hideBalanceQuery ||
|
||||||
|
isOpenAiUrl ||
|
||||||
|
accessStore.provider === ServiceProvider.Azure
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
accessStore.hideBalanceQuery,
|
||||||
|
accessStore.openaiUrl,
|
||||||
|
accessStore.provider,
|
||||||
|
]);
|
||||||
|
|
||||||
const usage = {
|
const usage = {
|
||||||
used: updateStore.used,
|
used: updateStore.used,
|
||||||
@@ -877,16 +888,16 @@ export function Settings() {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<List>
|
<List id={SlotID.CustomModel}>
|
||||||
{showAccessCode ? (
|
{showAccessCode && (
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.AccessCode.Title}
|
title={Locale.Settings.Access.AccessCode.Title}
|
||||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
subTitle={Locale.Settings.Access.AccessCode.SubTitle}
|
||||||
>
|
>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
value={accessStore.accessCode}
|
value={accessStore.accessCode}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={Locale.Settings.AccessCode.Placeholder}
|
placeholder={Locale.Settings.Access.AccessCode.Placeholder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.accessCode = e.currentTarget.value),
|
(access) => (access.accessCode = e.currentTarget.value),
|
||||||
@@ -894,44 +905,152 @@ export function Settings() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!accessStore.hideUserApiKey ? (
|
{!accessStore.hideUserApiKey && (
|
||||||
<>
|
<>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Endpoint.Title}
|
title={Locale.Settings.Access.CustomEndpoint.Title}
|
||||||
subTitle={Locale.Settings.Endpoint.SubTitle}
|
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="checkbox"
|
||||||
value={accessStore.openaiUrl}
|
checked={accessStore.useCustomConfig}
|
||||||
placeholder="https://api.openai.com/"
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.openaiUrl = e.currentTarget.value),
|
(access) =>
|
||||||
|
(access.useCustomConfig = e.currentTarget.checked),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
{accessStore.useCustomConfig && (
|
||||||
title={Locale.Settings.Token.Title}
|
<>
|
||||||
subTitle={Locale.Settings.Token.SubTitle}
|
<ListItem
|
||||||
>
|
title={Locale.Settings.Access.Provider.Title}
|
||||||
<PasswordInput
|
subTitle={Locale.Settings.Access.Provider.SubTitle}
|
||||||
value={accessStore.token}
|
>
|
||||||
type="text"
|
<Select
|
||||||
placeholder={Locale.Settings.Token.Placeholder}
|
value={accessStore.provider}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.token = e.currentTarget.value),
|
(access) =>
|
||||||
);
|
(access.provider = e.target
|
||||||
}}
|
.value as ServiceProvider),
|
||||||
/>
|
);
|
||||||
</ListItem>
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(ServiceProvider).map(([k, v]) => (
|
||||||
|
<option value={v} key={k}>
|
||||||
|
{k}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{accessStore.provider === "OpenAI" ? (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
|
||||||
|
subTitle={
|
||||||
|
Locale.Settings.Access.OpenAI.Endpoint.SubTitle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={accessStore.openaiUrl}
|
||||||
|
placeholder={OPENAI_BASE_URL}
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) =>
|
||||||
|
(access.openaiUrl = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.OpenAI.ApiKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
value={accessStore.openaiApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={
|
||||||
|
Locale.Settings.Access.OpenAI.ApiKey.Placeholder
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) =>
|
||||||
|
(access.openaiApiKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Azure.Endpoint.Title}
|
||||||
|
subTitle={
|
||||||
|
Locale.Settings.Access.Azure.Endpoint.SubTitle +
|
||||||
|
Azure.ExampleEndpoint
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={accessStore.azureUrl}
|
||||||
|
placeholder={Azure.ExampleEndpoint}
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) =>
|
||||||
|
(access.azureUrl = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Azure.ApiKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
value={accessStore.azureApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={
|
||||||
|
Locale.Settings.Access.Azure.ApiKey.Placeholder
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) =>
|
||||||
|
(access.azureApiKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.Azure.ApiVerion.Title}
|
||||||
|
subTitle={
|
||||||
|
Locale.Settings.Access.Azure.ApiVerion.SubTitle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={accessStore.azureApiVersion}
|
||||||
|
placeholder="2023-08-01-preview"
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) =>
|
||||||
|
(access.azureApiVersion =
|
||||||
|
e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
{!shouldHideBalanceQuery ? (
|
{!shouldHideBalanceQuery ? (
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -960,8 +1079,8 @@ export function Settings() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.CustomModel.Title}
|
title={Locale.Settings.Access.CustomModel.Title}
|
||||||
subTitle={Locale.Settings.CustomModel.SubTitle}
|
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useCallback, useMemo } from "react";
|
import { useEffect, useRef, useMemo } from "react";
|
||||||
|
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import GithubIcon from "../icons/github.svg";
|
|||||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import MaskIcon from "../icons/mask.svg";
|
import MaskIcon from "../icons/mask.svg";
|
||||||
import PluginIcon from "../icons/plugin.svg";
|
import PluginIcon from "../icons/plugin.svg";
|
||||||
import DragIcon from "../icons/drag.svg";
|
import DragIcon from "../icons/drag.svg";
|
||||||
@@ -202,7 +203,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
<div className={styles["sidebar-actions"]}>
|
<div className={styles["sidebar-actions"]}>
|
||||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<CloseIcon />}
|
icon={<DeleteIcon />}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
chatStore.deleteSession(chatStore.currentSessionIndex);
|
||||||
|
@@ -235,7 +235,7 @@
|
|||||||
.select-with-icon-select {
|
.select-with-icon-select {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
padding: 10px 25px 10px 10px;
|
padding: 10px 35px 10px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@@ -70,14 +70,12 @@ export function ListItem(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function List(props: {
|
export function List(props: { children: React.ReactNode; id?: string }) {
|
||||||
children:
|
return (
|
||||||
| Array<JSX.Element | null | undefined>
|
<div className={styles.list} id={props.id}>
|
||||||
| JSX.Element
|
{props.children}
|
||||||
| null
|
</div>
|
||||||
| undefined;
|
);
|
||||||
}) {
|
|
||||||
return <div className={styles.list}>{props.children}</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Loading() {
|
export function Loading() {
|
||||||
|
@@ -1,19 +1,31 @@
|
|||||||
import md5 from "spark-md5";
|
import md5 from "spark-md5";
|
||||||
|
import { DEFAULT_MODELS } from "../constant";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace NodeJS {
|
namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
|
PROXY_URL?: string; // docker only
|
||||||
|
|
||||||
OPENAI_API_KEY?: string;
|
OPENAI_API_KEY?: string;
|
||||||
CODE?: string;
|
CODE?: string;
|
||||||
|
|
||||||
BASE_URL?: string;
|
BASE_URL?: string;
|
||||||
PROXY_URL?: string;
|
OPENAI_ORG_ID?: string; // openai only
|
||||||
|
|
||||||
VERCEL?: string;
|
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_MODE?: "standalone" | "export";
|
||||||
BUILD_APP?: string; // is building desktop app
|
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
|
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
|
||||||
DISABLE_FAST_LINK?: string; // disallow parse settings from url 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,17 +50,39 @@ export const getServerSideConfig = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disableGPT4 = !!process.env.DISABLE_GPT4;
|
||||||
|
let customModels = process.env.CUSTOM_MODELS ?? "";
|
||||||
|
|
||||||
|
if (disableGPT4) {
|
||||||
|
if (customModels) customModels += ",";
|
||||||
|
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
|
||||||
|
.map((m) => "-" + m.name)
|
||||||
|
.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAzure = !!process.env.AZURE_URL;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
baseUrl: process.env.BASE_URL,
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
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,
|
code: process.env.CODE,
|
||||||
codes: ACCESS_CODES,
|
codes: ACCESS_CODES,
|
||||||
needCode: ACCESS_CODES.size > 0,
|
|
||||||
baseUrl: process.env.BASE_URL,
|
|
||||||
proxyUrl: process.env.PROXY_URL,
|
proxyUrl: process.env.PROXY_URL,
|
||||||
isVercel: !!process.env.VERCEL,
|
isVercel: !!process.env.VERCEL,
|
||||||
|
|
||||||
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
|
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
|
||||||
disableGPT4: !!process.env.DISABLE_GPT4,
|
disableGPT4,
|
||||||
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
|
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
|
||||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||||
|
customModels,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -23,10 +23,12 @@ export enum Path {
|
|||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
Cors = "/api/cors",
|
Cors = "/api/cors",
|
||||||
|
OpenAI = "/api/openai",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
AppBody = "app-body",
|
AppBody = "app-body",
|
||||||
|
CustomModel = "custom-model",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FileName {
|
export enum FileName {
|
||||||
@@ -60,6 +62,11 @@ export const REQUEST_TIMEOUT_MS = 60000;
|
|||||||
|
|
||||||
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
|
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
|
||||||
|
|
||||||
|
export enum ServiceProvider {
|
||||||
|
OpenAI = "OpenAI",
|
||||||
|
Azure = "Azure",
|
||||||
|
}
|
||||||
|
|
||||||
export const OpenaiPath = {
|
export const OpenaiPath = {
|
||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
UsagePath: "dashboard/billing/usage",
|
UsagePath: "dashboard/billing/usage",
|
||||||
@@ -67,6 +74,10 @@ export const OpenaiPath = {
|
|||||||
ListModelPath: "v1/models",
|
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_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||||
export const DEFAULT_SYSTEM_TEMPLATE = `
|
export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||||
You are ChatGPT, a large language model trained by OpenAI.
|
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<string, string> = {
|
export const KnowledgeCutOffDate: Record<string, string> = {
|
||||||
default: "2021-09",
|
default: "2021-09",
|
||||||
"gpt-3.5-turbo-1106": "2023-04",
|
|
||||||
"gpt-4-1106-preview": "2023-04",
|
"gpt-4-1106-preview": "2023-04",
|
||||||
"gpt-4-vision-preview": "2023-04",
|
"gpt-4-vision-preview": "2023-04",
|
||||||
};
|
};
|
||||||
|
@@ -167,11 +167,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
|
|||||||
Title: "حد الضغط للتاريخ",
|
Title: "حد الضغط للتاريخ",
|
||||||
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
|
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
|
||||||
},
|
},
|
||||||
Token: {
|
|
||||||
Title: "مفتاح API",
|
|
||||||
SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول",
|
|
||||||
Placeholder: "مفتاح OpenAI API",
|
|
||||||
},
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "رصيد الحساب",
|
Title: "رصيد الحساب",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -181,15 +177,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
|
|||||||
Check: "التحقق",
|
Check: "التحقق",
|
||||||
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
|
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "رمز الوصول",
|
|
||||||
SubTitle: "تم تمكين التحكم في الوصول",
|
|
||||||
Placeholder: "رمز الوصول المطلوب",
|
|
||||||
},
|
|
||||||
Endpoint: {
|
|
||||||
Title: "نقطة النهاية",
|
|
||||||
SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://",
|
|
||||||
},
|
|
||||||
Model: "النموذج",
|
Model: "النموذج",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "الحرارة",
|
Title: "الحرارة",
|
||||||
|
@@ -199,11 +199,7 @@ const bn: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
|
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
|
||||||
},
|
},
|
||||||
Token: {
|
|
||||||
Title: "অ্যাপি কী",
|
|
||||||
SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন",
|
|
||||||
Placeholder: "OpenAI API কী",
|
|
||||||
},
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "একাউন্ট ব্যালেন্স",
|
Title: "একাউন্ট ব্যালেন্স",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -213,15 +209,7 @@ const bn: PartialLocaleType = {
|
|||||||
Check: "চেক",
|
Check: "চেক",
|
||||||
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
|
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "অ্যাক্সেস কোড",
|
|
||||||
SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়",
|
|
||||||
Placeholder: "অ্যাক্সেস কোড প্রয়োজন",
|
|
||||||
},
|
|
||||||
Endpoint: {
|
|
||||||
Title: "ইনটারপয়েন্ট",
|
|
||||||
SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে",
|
|
||||||
},
|
|
||||||
Model: "মডেল",
|
Model: "মডেল",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "তাপমাত্রা",
|
Title: "তাপমাত্রা",
|
||||||
|
@@ -258,11 +258,6 @@ const cn = {
|
|||||||
Title: "历史消息长度压缩阈值",
|
Title: "历史消息长度压缩阈值",
|
||||||
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
|
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
|
||||||
},
|
},
|
||||||
Token: {
|
|
||||||
Title: "API Key",
|
|
||||||
SubTitle: "使用自己的 Key 可绕过密码访问限制",
|
|
||||||
Placeholder: "OpenAI API Key",
|
|
||||||
},
|
|
||||||
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "余额查询",
|
Title: "余额查询",
|
||||||
@@ -273,19 +268,56 @@ const cn = {
|
|||||||
Check: "重新检查",
|
Check: "重新检查",
|
||||||
NoAccess: "输入 API Key 或访问密码查看余额",
|
NoAccess: "输入 API Key 或访问密码查看余额",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "访问密码",
|
Access: {
|
||||||
SubTitle: "管理员已开启加密访问",
|
AccessCode: {
|
||||||
Placeholder: "请输入访问密码",
|
Title: "访问密码",
|
||||||
},
|
SubTitle: "管理员已开启加密访问",
|
||||||
Endpoint: {
|
Placeholder: "请输入访问密码",
|
||||||
Title: "接口地址",
|
},
|
||||||
SubTitle: "除默认地址外,必须包含 http(s)://",
|
CustomEndpoint: {
|
||||||
},
|
Title: "自定义接口",
|
||||||
CustomModel: {
|
SubTitle: "是否使用自定义 Azure 或 OpenAI 服务",
|
||||||
Title: "自定义模型名",
|
},
|
||||||
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
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)",
|
Model: "模型 (model)",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "随机性 (temperature)",
|
Title: "随机性 (temperature)",
|
||||||
|
@@ -124,11 +124,7 @@ const cs: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
|
"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: {
|
Usage: {
|
||||||
Title: "Stav účtu",
|
Title: "Stav účtu",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -138,11 +134,7 @@ const cs: PartialLocaleType = {
|
|||||||
Check: "Zkontrolovat",
|
Check: "Zkontrolovat",
|
||||||
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
|
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",
|
Model: "Model",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Teplota",
|
Title: "Teplota",
|
||||||
|
@@ -124,12 +124,7 @@ const de: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
|
"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: {
|
Usage: {
|
||||||
Title: "Kontostand",
|
Title: "Kontostand",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -139,11 +134,6 @@ const de: PartialLocaleType = {
|
|||||||
Check: "Erneut prüfen",
|
Check: "Erneut prüfen",
|
||||||
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
|
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "Zugangscode",
|
|
||||||
SubTitle: "Zugangskontrolle aktiviert",
|
|
||||||
Placeholder: "Zugangscode erforderlich",
|
|
||||||
},
|
|
||||||
Model: "Modell",
|
Model: "Modell",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperature", //Temperatur
|
Title: "Temperature", //Temperatur
|
||||||
|
@@ -262,11 +262,7 @@ 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",
|
|
||||||
},
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "Account Balance",
|
Title: "Account Balance",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -276,19 +272,55 @@ const en: LocaleType = {
|
|||||||
Check: "Check",
|
Check: "Check",
|
||||||
NoAccess: "Enter API Key to check balance",
|
NoAccess: "Enter API Key to check balance",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
Access: {
|
||||||
Title: "Access Code",
|
AccessCode: {
|
||||||
SubTitle: "Access control enabled",
|
Title: "Access Code",
|
||||||
Placeholder: "Need Access Code",
|
SubTitle: "Access control Enabled",
|
||||||
},
|
Placeholder: "Enter Code",
|
||||||
Endpoint: {
|
},
|
||||||
Title: "Endpoint",
|
CustomEndpoint: {
|
||||||
SubTitle: "Custom endpoint must start with http(s)://",
|
Title: "Custom Endpoint",
|
||||||
},
|
SubTitle: "Use custom Azure or OpenAI service",
|
||||||
CustomModel: {
|
},
|
||||||
Title: "Custom Models",
|
Provider: {
|
||||||
SubTitle: "Add extra model options, separate by comma",
|
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",
|
Model: "Model",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperature",
|
Title: "Temperature",
|
||||||
|
@@ -124,11 +124,7 @@ const es: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor",
|
"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: {
|
Usage: {
|
||||||
Title: "Saldo de la cuenta",
|
Title: "Saldo de la cuenta",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -138,11 +134,7 @@ const es: PartialLocaleType = {
|
|||||||
Check: "Comprobar de nuevo",
|
Check: "Comprobar de nuevo",
|
||||||
NoAccess: "Introduzca la clave API para comprobar el saldo",
|
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",
|
Model: "Modelo",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperatura",
|
Title: "Temperatura",
|
||||||
|
@@ -173,11 +173,7 @@ const fr: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Comprimera si la longueur des messages non compressés dépasse cette valeur",
|
"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: {
|
Usage: {
|
||||||
Title: "Solde du compte",
|
Title: "Solde du compte",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -187,11 +183,7 @@ const fr: PartialLocaleType = {
|
|||||||
Check: "Vérifier",
|
Check: "Vérifier",
|
||||||
NoAccess: "Entrez la clé API pour vérifier le solde",
|
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",
|
Model: "Modèle",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Température",
|
Title: "Température",
|
||||||
|
@@ -4,8 +4,9 @@ import { PartialLocaleType } from "./index";
|
|||||||
const id: PartialLocaleType = {
|
const id: PartialLocaleType = {
|
||||||
WIP: "Coming Soon...",
|
WIP: "Coming Soon...",
|
||||||
Error: {
|
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: {
|
Auth: {
|
||||||
Title: "Diperlukan Kode Akses",
|
Title: "Diperlukan Kode Akses",
|
||||||
Tips: "Masukkan kode akses di bawah",
|
Tips: "Masukkan kode akses di bawah",
|
||||||
@@ -237,11 +238,7 @@ const id: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
|
"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: {
|
Usage: {
|
||||||
Title: "Saldo Akun",
|
Title: "Saldo Akun",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -251,15 +248,7 @@ const id: PartialLocaleType = {
|
|||||||
Check: "Periksa",
|
Check: "Periksa",
|
||||||
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
|
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",
|
Model: "Model",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Suhu",
|
Title: "Suhu",
|
||||||
|
@@ -124,12 +124,7 @@ const it: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
|
"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: {
|
Usage: {
|
||||||
Title: "Bilancio Account",
|
Title: "Bilancio Account",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -139,11 +134,7 @@ const it: PartialLocaleType = {
|
|||||||
Check: "Controlla ancora",
|
Check: "Controlla ancora",
|
||||||
NoAccess: "Inserire la chiave API per controllare il saldo",
|
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",
|
Model: "Modello GPT",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperature",
|
Title: "Temperature",
|
||||||
|
@@ -20,7 +20,8 @@ const jp: PartialLocaleType = {
|
|||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "リトライ",
|
Retry: "リトライ",
|
||||||
Pin: "ピン",
|
Pin: "ピン",
|
||||||
PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました",
|
PinToastContent:
|
||||||
|
"コンテキストプロンプトに1つのメッセージをピン留めしました",
|
||||||
PinToastAction: "表示",
|
PinToastAction: "表示",
|
||||||
Delete: "削除",
|
Delete: "削除",
|
||||||
Edit: "編集",
|
Edit: "編集",
|
||||||
@@ -146,11 +147,7 @@ const jp: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
|
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
|
||||||
},
|
},
|
||||||
Token: {
|
|
||||||
Title: "APIキー",
|
|
||||||
SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する",
|
|
||||||
Placeholder: "OpenAI APIキー",
|
|
||||||
},
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "残高照会",
|
Title: "残高照会",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -160,11 +157,7 @@ const jp: PartialLocaleType = {
|
|||||||
Check: "再確認",
|
Check: "再確認",
|
||||||
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
|
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "アクセスパスワード",
|
|
||||||
SubTitle: "暗号化アクセスが有効になっています",
|
|
||||||
Placeholder: "アクセスパスワードを入力してください",
|
|
||||||
},
|
|
||||||
Model: "モデル (model)",
|
Model: "モデル (model)",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "ランダム性 (temperature)",
|
Title: "ランダム性 (temperature)",
|
||||||
|
@@ -124,11 +124,7 @@ const ko: PartialLocaleType = {
|
|||||||
Title: "기록 압축 임계값",
|
Title: "기록 압축 임계값",
|
||||||
SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨",
|
SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨",
|
||||||
},
|
},
|
||||||
Token: {
|
|
||||||
Title: "API 키",
|
|
||||||
SubTitle: "액세스 코드 제한을 무시하기 위해 키 사용",
|
|
||||||
Placeholder: "OpenAI API 키",
|
|
||||||
},
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "계정 잔액",
|
Title: "계정 잔액",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -138,11 +134,7 @@ const ko: PartialLocaleType = {
|
|||||||
Check: "확인",
|
Check: "확인",
|
||||||
NoAccess: "잔액 확인을 위해 API 키를 입력하세요.",
|
NoAccess: "잔액 확인을 위해 API 키를 입력하세요.",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "액세스 코드",
|
|
||||||
SubTitle: "액세스 제어가 활성화됨",
|
|
||||||
Placeholder: "액세스 코드 입력",
|
|
||||||
},
|
|
||||||
Model: "모델",
|
Model: "모델",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "온도 (temperature)",
|
Title: "온도 (temperature)",
|
||||||
|
@@ -106,12 +106,7 @@ const no: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien",
|
"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: {
|
Usage: {
|
||||||
Title: "Saldo for konto",
|
Title: "Saldo for konto",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -121,11 +116,7 @@ const no: PartialLocaleType = {
|
|||||||
Check: "Sjekk",
|
Check: "Sjekk",
|
||||||
NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo",
|
NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "Tilgangskode",
|
|
||||||
SubTitle: "Tilgangskontroll på",
|
|
||||||
Placeholder: "Trenger tilgangskode",
|
|
||||||
},
|
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperatur",
|
Title: "Temperatur",
|
||||||
|
@@ -125,11 +125,7 @@ const ru: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
|
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
|
||||||
},
|
},
|
||||||
Token: {
|
|
||||||
Title: "API ключ",
|
|
||||||
SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
|
|
||||||
Placeholder: "API ключ OpenAI",
|
|
||||||
},
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "Баланс аккаунта",
|
Title: "Баланс аккаунта",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -139,11 +135,7 @@ const ru: PartialLocaleType = {
|
|||||||
Check: "Проверить",
|
Check: "Проверить",
|
||||||
NoAccess: "Введите API ключ, чтобы проверить баланс",
|
NoAccess: "Введите API ключ, чтобы проверить баланс",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "Код доступа",
|
|
||||||
SubTitle: "Контроль доступа включен",
|
|
||||||
Placeholder: "Требуется код доступа",
|
|
||||||
},
|
|
||||||
Model: "Модель",
|
Model: "Модель",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Температура",
|
Title: "Температура",
|
||||||
|
@@ -124,11 +124,7 @@ const tr: PartialLocaleType = {
|
|||||||
SubTitle:
|
SubTitle:
|
||||||
"Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır",
|
"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: {
|
Usage: {
|
||||||
Title: "Hesap Bakiyesi",
|
Title: "Hesap Bakiyesi",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -138,11 +134,7 @@ const tr: PartialLocaleType = {
|
|||||||
Check: "Tekrar Kontrol Et",
|
Check: "Tekrar Kontrol Et",
|
||||||
NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin",
|
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",
|
Model: "Model",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Gerçeklik",
|
Title: "Gerçeklik",
|
||||||
|
@@ -120,11 +120,7 @@ const tw: PartialLocaleType = {
|
|||||||
Title: "歷史訊息長度壓縮閾值",
|
Title: "歷史訊息長度壓縮閾值",
|
||||||
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
|
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
|
||||||
},
|
},
|
||||||
Token: {
|
|
||||||
Title: "API Key",
|
|
||||||
SubTitle: "使用自己的 Key 可規避授權存取限制",
|
|
||||||
Placeholder: "OpenAI API Key",
|
|
||||||
},
|
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "帳戶餘額",
|
Title: "帳戶餘額",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -134,11 +130,7 @@ const tw: PartialLocaleType = {
|
|||||||
Check: "重新檢查",
|
Check: "重新檢查",
|
||||||
NoAccess: "輸入 API Key 檢視餘額",
|
NoAccess: "輸入 API Key 檢視餘額",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
|
||||||
Title: "授權碼",
|
|
||||||
SubTitle: "目前是未授權存取狀態",
|
|
||||||
Placeholder: "請輸入授權碼",
|
|
||||||
},
|
|
||||||
Model: "模型 (model)",
|
Model: "模型 (model)",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "隨機性 (temperature)",
|
Title: "隨機性 (temperature)",
|
||||||
|
@@ -123,11 +123,7 @@ const vi: PartialLocaleType = {
|
|||||||
Title: "Ngưỡng nén lịch sử tin nhắn",
|
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",
|
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: {
|
Usage: {
|
||||||
Title: "Hạn mức tài khoản",
|
Title: "Hạn mức tài khoản",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
@@ -137,11 +133,7 @@ const vi: PartialLocaleType = {
|
|||||||
Check: "Kiểm tra",
|
Check: "Kiểm tra",
|
||||||
NoAccess: "Nhập API Key để kiểm tra hạn mức",
|
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",
|
Model: "Mô hình",
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Tính ngẫu nhiên (temperature)",
|
Title: "Tính ngẫu nhiên (temperature)",
|
||||||
|
@@ -1,24 +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 { getHeaders } from "../client/api";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
|
import { ensure } from "../utils/clone";
|
||||||
|
|
||||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||||
|
|
||||||
const DEFAULT_OPENAI_URL =
|
const DEFAULT_OPENAI_URL =
|
||||||
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
|
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI;
|
||||||
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
|
|
||||||
|
|
||||||
const DEFAULT_ACCESS_STATE = {
|
const DEFAULT_ACCESS_STATE = {
|
||||||
token: "",
|
|
||||||
accessCode: "",
|
accessCode: "",
|
||||||
|
useCustomConfig: false,
|
||||||
|
|
||||||
|
provider: ServiceProvider.OpenAI,
|
||||||
|
|
||||||
|
// openai
|
||||||
|
openaiUrl: DEFAULT_OPENAI_URL,
|
||||||
|
openaiApiKey: "",
|
||||||
|
|
||||||
|
// azure
|
||||||
|
azureUrl: "",
|
||||||
|
azureApiKey: "",
|
||||||
|
azureApiVersion: "2023-08-01-preview",
|
||||||
|
|
||||||
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
hideBalanceQuery: false,
|
hideBalanceQuery: false,
|
||||||
disableGPT4: false,
|
disableGPT4: false,
|
||||||
disableFastLink: false,
|
disableFastLink: false,
|
||||||
|
customModels: "",
|
||||||
openaiUrl: DEFAULT_OPENAI_URL,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAccessStore = createPersistStore(
|
export const useAccessStore = createPersistStore(
|
||||||
@@ -30,12 +47,24 @@ export const useAccessStore = createPersistStore(
|
|||||||
|
|
||||||
return get().needCode;
|
return get().needCode;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidOpenAI() {
|
||||||
|
return ensure(get(), ["openaiUrl", "openaiApiKey"]);
|
||||||
|
},
|
||||||
|
|
||||||
|
isValidAzure() {
|
||||||
|
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
||||||
|
},
|
||||||
|
|
||||||
isAuthorized() {
|
isAuthorized() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
|
|
||||||
// has token or has code or disabled access control
|
// has token or has code or disabled access control
|
||||||
return (
|
return (
|
||||||
!!get().token || !!get().accessCode || !this.enabledAccessControl()
|
this.isValidOpenAI() ||
|
||||||
|
this.isValidAzure() ||
|
||||||
|
!this.enabledAccessControl() ||
|
||||||
|
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
fetch() {
|
fetch() {
|
||||||
@@ -52,12 +81,6 @@ export const useAccessStore = createPersistStore(
|
|||||||
.then((res: DangerConfig) => {
|
.then((res: DangerConfig) => {
|
||||||
console.log("[Config] got config from server", res);
|
console.log("[Config] got config from server", res);
|
||||||
set(() => ({ ...res }));
|
set(() => ({ ...res }));
|
||||||
|
|
||||||
if (res.disableGPT4) {
|
|
||||||
DEFAULT_MODELS.forEach(
|
|
||||||
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error("[Config] failed to fetch config");
|
console.error("[Config] failed to fetch config");
|
||||||
@@ -69,6 +92,19 @@ export const useAccessStore = createPersistStore(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Access,
|
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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@@ -85,33 +85,6 @@ function getSummarizeModel(currentModel: string) {
|
|||||||
return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel;
|
return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatStore {
|
|
||||||
sessions: ChatSession[];
|
|
||||||
currentSessionIndex: number;
|
|
||||||
clearSessions: () => void;
|
|
||||||
moveSession: (from: number, to: number) => void;
|
|
||||||
selectSession: (index: number) => void;
|
|
||||||
newSession: (mask?: Mask) => void;
|
|
||||||
deleteSession: (index: number) => void;
|
|
||||||
currentSession: () => ChatSession;
|
|
||||||
nextSession: (delta: number) => void;
|
|
||||||
onNewMessage: (message: ChatMessage) => void;
|
|
||||||
onUserInput: (content: string) => Promise<void>;
|
|
||||||
summarizeSession: () => void;
|
|
||||||
updateStat: (message: ChatMessage) => void;
|
|
||||||
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
|
|
||||||
updateMessage: (
|
|
||||||
sessionIndex: number,
|
|
||||||
messageIndex: number,
|
|
||||||
updater: (message?: ChatMessage) => void,
|
|
||||||
) => void;
|
|
||||||
resetSession: () => void;
|
|
||||||
getMessagesWithMemory: () => ChatMessage[];
|
|
||||||
getMemoryPrompt: () => ChatMessage;
|
|
||||||
|
|
||||||
clearAllData: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function countMessages(msgs: ChatMessage[]) {
|
function countMessages(msgs: ChatMessage[]) {
|
||||||
return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
|
return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
|
||||||
}
|
}
|
||||||
|
@@ -49,7 +49,7 @@ export const DEFAULT_CONFIG = {
|
|||||||
model: "gpt-3.5-turbo" as ModelType,
|
model: "gpt-3.5-turbo" as ModelType,
|
||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
top_p: 1,
|
top_p: 1,
|
||||||
max_tokens: 2000,
|
max_tokens: 4000,
|
||||||
presence_penalty: 0,
|
presence_penalty: 0,
|
||||||
frequency_penalty: 0,
|
frequency_penalty: 0,
|
||||||
sendMemory: true,
|
sendMemory: true,
|
||||||
@@ -82,7 +82,7 @@ export const ModalConfigValidator = {
|
|||||||
return x as ModelType;
|
return x as ModelType;
|
||||||
},
|
},
|
||||||
max_tokens(x: number) {
|
max_tokens(x: number) {
|
||||||
return limitNumber(x, 0, 100000, 2000);
|
return limitNumber(x, 0, 512000, 1024);
|
||||||
},
|
},
|
||||||
presence_penalty(x: number) {
|
presence_penalty(x: number) {
|
||||||
return limitNumber(x, -2, 2, 0);
|
return limitNumber(x, -2, 2, 0);
|
||||||
@@ -128,15 +128,7 @@ export const useAppConfig = createPersistStore(
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
allModels() {
|
allModels() {},
|
||||||
const customModels = get()
|
|
||||||
.customModels.split(",")
|
|
||||||
.filter((v) => !!v && v.length > 0)
|
|
||||||
.map((m) => ({ name: m, available: true }));
|
|
||||||
const allModels = get().models.concat(customModels);
|
|
||||||
allModels.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
||||||
return allModels;
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Config,
|
name: StoreKey.Config,
|
||||||
|
@@ -357,3 +357,7 @@ pre {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copyable {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
@@ -1,3 +1,12 @@
|
|||||||
export function deepClone<T>(obj: T) {
|
export function deepClone<T>(obj: T) {
|
||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ensure<T extends object>(
|
||||||
|
obj: T,
|
||||||
|
keys: Array<[keyof T][number]>,
|
||||||
|
) {
|
||||||
|
return keys.every(
|
||||||
|
(k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
16
app/utils/hooks.ts
Normal file
16
app/utils/hooks.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useAccessStore, useAppConfig } from "../store";
|
||||||
|
import { collectModels } from "./model";
|
||||||
|
|
||||||
|
export function useAllModels() {
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const configStore = useAppConfig();
|
||||||
|
const models = useMemo(() => {
|
||||||
|
return collectModels(
|
||||||
|
configStore.models,
|
||||||
|
[accessStore.customModels, configStore.customModels].join(","),
|
||||||
|
);
|
||||||
|
}, [accessStore.customModels, configStore.customModels, configStore.models]);
|
||||||
|
|
||||||
|
return models;
|
||||||
|
}
|
40
app/utils/model.ts
Normal file
40
app/utils/model.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { LLMModel } from "../client/api";
|
||||||
|
|
||||||
|
export function collectModelTable(
|
||||||
|
models: readonly LLMModel[],
|
||||||
|
customModels: string,
|
||||||
|
) {
|
||||||
|
const modelTable: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
// default models
|
||||||
|
models.forEach((m) => (modelTable[m.name] = m.available));
|
||||||
|
|
||||||
|
// server custom models
|
||||||
|
customModels
|
||||||
|
.split(",")
|
||||||
|
.filter((v) => !!v && v.length > 0)
|
||||||
|
.map((m) => {
|
||||||
|
if (m.startsWith("+")) {
|
||||||
|
modelTable[m.slice(1)] = true;
|
||||||
|
} else if (m.startsWith("-")) {
|
||||||
|
modelTable[m.slice(1)] = false;
|
||||||
|
} else modelTable[m] = true;
|
||||||
|
});
|
||||||
|
return modelTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate full model table.
|
||||||
|
*/
|
||||||
|
export function collectModels(
|
||||||
|
models: readonly LLMModel[],
|
||||||
|
customModels: string,
|
||||||
|
) {
|
||||||
|
const modelTable = collectModelTable(models, customModels);
|
||||||
|
const allModels = Object.keys(modelTable).map((m) => ({
|
||||||
|
name: m,
|
||||||
|
available: modelTable[m],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return allModels;
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { combine, persist } from "zustand/middleware";
|
||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { deepClone } from "./clone";
|
import { deepClone } from "./clone";
|
||||||
|
|
||||||
@@ -23,33 +23,42 @@ type SetStoreState<T> = (
|
|||||||
replace?: boolean | undefined,
|
replace?: boolean | undefined,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export function createPersistStore<T, M>(
|
export function createPersistStore<T extends object, M>(
|
||||||
defaultState: T,
|
state: T,
|
||||||
methods: (
|
methods: (
|
||||||
set: SetStoreState<T & MakeUpdater<T>>,
|
set: SetStoreState<T & MakeUpdater<T>>,
|
||||||
get: () => T & MakeUpdater<T>,
|
get: () => T & MakeUpdater<T>,
|
||||||
) => M,
|
) => M,
|
||||||
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
|
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
|
||||||
) {
|
) {
|
||||||
return create<T & M & MakeUpdater<T>>()(
|
return create(
|
||||||
persist((set, get) => {
|
persist(
|
||||||
return {
|
combine(
|
||||||
...defaultState,
|
{
|
||||||
...methods(set as any, get),
|
...state,
|
||||||
|
lastUpdateTime: 0,
|
||||||
|
},
|
||||||
|
(set, get) => {
|
||||||
|
return {
|
||||||
|
...methods(set, get as any),
|
||||||
|
|
||||||
lastUpdateTime: 0,
|
markUpdate() {
|
||||||
markUpdate() {
|
set({ lastUpdateTime: Date.now() } as Partial<
|
||||||
set({ lastUpdateTime: Date.now() } as Partial<
|
T & M & MakeUpdater<T>
|
||||||
T & M & MakeUpdater<T>
|
>);
|
||||||
>);
|
},
|
||||||
|
update(updater) {
|
||||||
|
const state = deepClone(get());
|
||||||
|
updater(state);
|
||||||
|
set({
|
||||||
|
...state,
|
||||||
|
lastUpdateTime: Date.now(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as M & MakeUpdater<T>;
|
||||||
},
|
},
|
||||||
update(updater) {
|
),
|
||||||
const state = deepClone(get());
|
persistOptions as any,
|
||||||
updater(state);
|
),
|
||||||
get().markUpdate();
|
|
||||||
set(state);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, persistOptions),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
version: '3.9'
|
version: "3.9"
|
||||||
services:
|
services:
|
||||||
chatgpt-next-web:
|
chatgpt-next-web:
|
||||||
profiles: ["no-proxy"]
|
profiles: ["no-proxy"]
|
||||||
container_name: chatgpt-next-web
|
container_name: chatgpt-next-web
|
||||||
image: yidadaa/chatgpt-next-web
|
image: yidadaa/chatgpt-next-web
|
||||||
@@ -13,8 +13,11 @@ services:
|
|||||||
- OPENAI_ORG_ID=$OPENAI_ORG_ID
|
- OPENAI_ORG_ID=$OPENAI_ORG_ID
|
||||||
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
|
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
|
||||||
- DISABLE_GPT4=$DISABLE_GPT4
|
- DISABLE_GPT4=$DISABLE_GPT4
|
||||||
|
- ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY
|
||||||
|
- DISABLE_FAST_LINK=$DISABLE_FAST_LINK
|
||||||
|
- OPENAI_SB=$OPENAI_SB
|
||||||
|
|
||||||
chatgpt-next-web-proxy:
|
chatgpt-next-web-proxy:
|
||||||
profiles: ["proxy"]
|
profiles: ["proxy"]
|
||||||
container_name: chatgpt-next-web-proxy
|
container_name: chatgpt-next-web-proxy
|
||||||
image: yidadaa/chatgpt-next-web
|
image: yidadaa/chatgpt-next-web
|
||||||
@@ -28,3 +31,6 @@ services:
|
|||||||
- OPENAI_ORG_ID=$OPENAI_ORG_ID
|
- OPENAI_ORG_ID=$OPENAI_ORG_ID
|
||||||
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
|
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
|
||||||
- DISABLE_GPT4=$DISABLE_GPT4
|
- DISABLE_GPT4=$DISABLE_GPT4
|
||||||
|
- ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY
|
||||||
|
- DISABLE_FAST_LINK=$DISABLE_FAST_LINK
|
||||||
|
- OPENAI_SB=$OPENAI_SB
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
# Cloudflare Pages 部署指南
|
# Cloudflare Pages 部署指南
|
||||||
|
|
||||||
## 如何新建项目
|
## 如何新建项目
|
||||||
|
|
||||||
在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。
|
在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。
|
||||||
|
|
||||||
1. 点击 "Create a project"。
|
1. 点击 "Create a project"。
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。
|
7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。
|
||||||
8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令:
|
8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令:
|
||||||
```
|
```
|
||||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
|
npx @cloudflare/next-on-pages@1.5.0
|
||||||
```
|
```
|
||||||
9. 对于 "Build output directory",使用默认值并且不要修改。
|
9. 对于 "Build output directory",使用默认值并且不要修改。
|
||||||
10. 不要修改 "Root Directory"。
|
10. 不要修改 "Root Directory"。
|
||||||
@@ -30,10 +31,12 @@
|
|||||||
- `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID`
|
- `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID`
|
||||||
- `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key`
|
- `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key`
|
||||||
- `DISABLE_GPT4=1 可选,不让用户使用 GPT-4`
|
- `DISABLE_GPT4=1 可选,不让用户使用 GPT-4`
|
||||||
|
- `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能`
|
||||||
|
- `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置`
|
||||||
|
|
||||||
12. 点击 "Save and Deploy"。
|
12. 点击 "Save and Deploy"。
|
||||||
13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。
|
13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。
|
||||||
14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。
|
14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。
|
||||||
15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。
|
15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。
|
||||||
16. 前往 "Deployments",点击 "Retry deployment"。
|
16. 前往 "Deployments",点击 "Retry deployment"。
|
||||||
17. Enjoy.
|
17. Enjoy.
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
# Cloudflare Pages Deployment Guide
|
# Cloudflare Pages Deployment Guide
|
||||||
|
|
||||||
## How to create a new project
|
## How to create a new project
|
||||||
|
|
||||||
Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
||||||
|
|
||||||
1. Click "Create a project".
|
1. Click "Create a project".
|
||||||
@@ -11,12 +12,13 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
|||||||
6. For "Project name" and "Production branch", use the default values or change them as needed.
|
6. For "Project name" and "Production branch", use the default values or change them as needed.
|
||||||
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
|
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
|
||||||
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
|
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
|
||||||
```
|
```
|
||||||
npx @cloudflare/next-on-pages --experimental-minify
|
npx @cloudflare/next-on-pages --experimental-minify
|
||||||
```
|
```
|
||||||
9. For "Build output directory", use the default value and do not modify it.
|
9. For "Build output directory", use the default value and do not modify it.
|
||||||
10. Do not modify "Root Directory".
|
10. Do not modify "Root Directory".
|
||||||
11. For "Environment variables", click ">" and then "Add variable". Fill in the following information:
|
11. For "Environment variables", click ">" and then "Add variable". Fill in the following information:
|
||||||
|
|
||||||
- `NODE_VERSION=20.1`
|
- `NODE_VERSION=20.1`
|
||||||
- `NEXT_TELEMETRY_DISABLE=1`
|
- `NEXT_TELEMETRY_DISABLE=1`
|
||||||
- `OPENAI_API_KEY=your_own_API_key`
|
- `OPENAI_API_KEY=your_own_API_key`
|
||||||
@@ -29,7 +31,10 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
|||||||
- `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI`
|
- `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI`
|
||||||
- `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key`
|
- `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key`
|
||||||
- `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4`
|
- `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4`
|
||||||
|
- `ENABLE_BALANCE_QUERY=1 Optional, allow users to query balance`
|
||||||
|
- `DISABLE_FAST_LINK=1 Optional, disable parse settings from url`
|
||||||
|
- `OPENAI_SB=1 Optional,use the third-party OpenAI-SB API`
|
||||||
|
|
||||||
12. Click "Save and Deploy".
|
12. Click "Save and Deploy".
|
||||||
13. Click "Cancel deployment" because you need to fill in Compatibility flags.
|
13. Click "Cancel deployment" because you need to fill in Compatibility flags.
|
||||||
14. Go to "Build settings", "Functions", and find "Compatibility flags".
|
14. Go to "Build settings", "Functions", and find "Compatibility flags".
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "ChatGPT Next Web",
|
"productName": "ChatGPT Next Web",
|
||||||
"version": "2.9.10"
|
"version": "2.9.11"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
Reference in New Issue
Block a user