mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-04-27 05:24:26 +08:00
Compare commits
14 Commits
dfd3d24004
...
feat-bt-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb7a1e853 | ||
|
|
fb2c15567d | ||
|
|
c2c52a1f60 | ||
|
|
106ddc17cd | ||
|
|
17d5209738 | ||
|
|
d66bfc6352 | ||
|
|
4d75b23ed1 | ||
|
|
36bfa2ef7c | ||
|
|
afe12c212e | ||
|
|
7a8d557ea3 | ||
|
|
d3f0a77830 | ||
|
|
0581e37236 | ||
|
|
44383a8b33 | ||
|
|
d357b45e84 |
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -19,26 +19,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Extract metadata (tags, labels) for Docker
|
name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: ${{ secrets.DOCKER_USERNAME }}/chatgpt-next-web
|
images: yidadaa/chatgpt-next-web
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Build and push Docker image
|
name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -49,4 +49,4 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
|||||||
@@ -397,6 +397,9 @@ yarn dev
|
|||||||
|
|
||||||
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
|
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
|
||||||
|
|
||||||
|
### BT Install
|
||||||
|
> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
|
||||||
|
|
||||||
### Docker (Recommended)
|
### Docker (Recommended)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
@@ -264,6 +264,9 @@ BASE_URL=https://b.nextweb.fun/api/proxy
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
|
### 宝塔面板部署
|
||||||
|
> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
|
||||||
|
|
||||||
### 容器部署 (推荐)
|
### 容器部署 (推荐)
|
||||||
|
|
||||||
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
|
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { handle as moonshotHandler } from "../../moonshot";
|
|||||||
import { handle as stabilityHandler } from "../../stability";
|
import { handle as stabilityHandler } from "../../stability";
|
||||||
import { handle as iflytekHandler } from "../../iflytek";
|
import { handle as iflytekHandler } from "../../iflytek";
|
||||||
import { handle as xaiHandler } from "../../xai";
|
import { handle as xaiHandler } from "../../xai";
|
||||||
|
import { handle as chatglmHandler } from "../../glm";
|
||||||
import { handle as proxyHandler } from "../../proxy";
|
import { handle as proxyHandler } from "../../proxy";
|
||||||
|
|
||||||
async function handle(
|
async function handle(
|
||||||
@@ -41,6 +42,8 @@ async function handle(
|
|||||||
return iflytekHandler(req, { params });
|
return iflytekHandler(req, { params });
|
||||||
case ApiPath.XAI:
|
case ApiPath.XAI:
|
||||||
return xaiHandler(req, { params });
|
return xaiHandler(req, { params });
|
||||||
|
case ApiPath.ChatGLM:
|
||||||
|
return chatglmHandler(req, { params });
|
||||||
case ApiPath.OpenAI:
|
case ApiPath.OpenAI:
|
||||||
return openaiHandler(req, { params });
|
return openaiHandler(req, { params });
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
|||||||
case ModelProvider.XAI:
|
case ModelProvider.XAI:
|
||||||
systemApiKey = serverConfig.xaiApiKey;
|
systemApiKey = serverConfig.xaiApiKey;
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.ChatGLM:
|
||||||
|
systemApiKey = serverConfig.chatglmApiKey;
|
||||||
|
break;
|
||||||
case ModelProvider.GPT:
|
case ModelProvider.GPT:
|
||||||
default:
|
default:
|
||||||
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
||||||
|
|||||||
129
app/api/glm.ts
Normal file
129
app/api/glm.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import {
|
||||||
|
CHATGLM_BASE_URL,
|
||||||
|
ApiPath,
|
||||||
|
ModelProvider,
|
||||||
|
ServiceProvider,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/api/auth";
|
||||||
|
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
export async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[GLM Route] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.ChatGLM);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(req);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[GLM] ", e);
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request(req: NextRequest) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// alibaba use base url or just remove the path
|
||||||
|
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}${path}`;
|
||||||
|
console.log("[Fetch Url] ", fetchUrl);
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: req.headers.get("Authorization") ?? "",
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
// #1815 try to refuse some request to some models
|
||||||
|
if (serverConfig.customModels && req.body) {
|
||||||
|
try {
|
||||||
|
const clonedBody = await req.text();
|
||||||
|
fetchOptions.body = clonedBody;
|
||||||
|
|
||||||
|
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||||
|
|
||||||
|
// not undefined and is false
|
||||||
|
if (
|
||||||
|
isModelAvailableInServer(
|
||||||
|
serverConfig.customModels,
|
||||||
|
jsonBody?.model as string,
|
||||||
|
ServiceProvider.ChatGLM as string,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[GLM] filter`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { HunyuanApi } from "./platforms/tencent";
|
|||||||
import { MoonshotApi } from "./platforms/moonshot";
|
import { MoonshotApi } from "./platforms/moonshot";
|
||||||
import { SparkApi } from "./platforms/iflytek";
|
import { SparkApi } from "./platforms/iflytek";
|
||||||
import { XAIApi } from "./platforms/xai";
|
import { XAIApi } from "./platforms/xai";
|
||||||
|
import { ChatGLMApi } from "./platforms/glm";
|
||||||
|
|
||||||
export const ROLES = ["system", "user", "assistant"] as const;
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
export type MessageRole = (typeof ROLES)[number];
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
@@ -156,6 +157,9 @@ export class ClientApi {
|
|||||||
case ModelProvider.XAI:
|
case ModelProvider.XAI:
|
||||||
this.llm = new XAIApi();
|
this.llm = new XAIApi();
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.ChatGLM:
|
||||||
|
this.llm = new ChatGLMApi();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.llm = new ChatGPTApi();
|
this.llm = new ChatGPTApi();
|
||||||
}
|
}
|
||||||
@@ -244,6 +248,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
||||||
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
||||||
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
|
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
|
||||||
|
const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
|
||||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||||
const apiKey = isGoogle
|
const apiKey = isGoogle
|
||||||
? accessStore.googleApiKey
|
? accessStore.googleApiKey
|
||||||
@@ -259,6 +264,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
? accessStore.moonshotApiKey
|
? accessStore.moonshotApiKey
|
||||||
: isXAI
|
: isXAI
|
||||||
? accessStore.xaiApiKey
|
? accessStore.xaiApiKey
|
||||||
|
: isChatGLM
|
||||||
|
? accessStore.chatglmApiKey
|
||||||
: isIflytek
|
: isIflytek
|
||||||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||||
@@ -274,6 +281,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
isMoonshot,
|
isMoonshot,
|
||||||
isIflytek,
|
isIflytek,
|
||||||
isXAI,
|
isXAI,
|
||||||
|
isChatGLM,
|
||||||
apiKey,
|
apiKey,
|
||||||
isEnabledAccessControl,
|
isEnabledAccessControl,
|
||||||
};
|
};
|
||||||
@@ -338,6 +346,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
|||||||
return new ClientApi(ModelProvider.Iflytek);
|
return new ClientApi(ModelProvider.Iflytek);
|
||||||
case ServiceProvider.XAI:
|
case ServiceProvider.XAI:
|
||||||
return new ClientApi(ModelProvider.XAI);
|
return new ClientApi(ModelProvider.XAI);
|
||||||
|
case ServiceProvider.ChatGLM:
|
||||||
|
return new ClientApi(ModelProvider.ChatGLM);
|
||||||
default:
|
default:
|
||||||
return new ClientApi(ModelProvider.GPT);
|
return new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
|||||||
197
app/client/platforms/glm.ts
Normal file
197
app/client/platforms/glm.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
ApiPath,
|
||||||
|
CHATGLM_BASE_URL,
|
||||||
|
ChatGLM,
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import {
|
||||||
|
useAccessStore,
|
||||||
|
useAppConfig,
|
||||||
|
useChatStore,
|
||||||
|
ChatMessageTool,
|
||||||
|
usePluginStore,
|
||||||
|
} from "@/app/store";
|
||||||
|
import { stream } from "@/app/utils/chat";
|
||||||
|
import {
|
||||||
|
ChatOptions,
|
||||||
|
getHeaders,
|
||||||
|
LLMApi,
|
||||||
|
LLMModel,
|
||||||
|
SpeechOptions,
|
||||||
|
} from "../api";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
import { RequestPayload } from "./openai";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
|
export class ChatGLMApi implements LLMApi {
|
||||||
|
private disableListModels = true;
|
||||||
|
|
||||||
|
path(path: string): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.chatglmUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
const apiPath = ApiPath.ChatGLM;
|
||||||
|
baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
|
||||||
|
baseUrl = "https://" + baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||||
|
|
||||||
|
return [baseUrl, path].join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
extractMessage(res: any) {
|
||||||
|
return res.choices?.at(0)?.message?.content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(options: ChatOptions) {
|
||||||
|
const messages: ChatOptions["messages"] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = getMessageTextContent(v);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
providerName: options.config.providerName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = {
|
||||||
|
messages,
|
||||||
|
stream: options.config.stream,
|
||||||
|
model: modelConfig.model,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
presence_penalty: modelConfig.presence_penalty,
|
||||||
|
frequency_penalty: modelConfig.frequency_penalty,
|
||||||
|
top_p: modelConfig.top_p,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[Request] glm payload: ", requestPayload);
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatPath = this.path(ChatGLM.ChatPath);
|
||||||
|
const chatPayload = {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: getHeaders(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldStream) {
|
||||||
|
const [tools, funcs] = usePluginStore
|
||||||
|
.getState()
|
||||||
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
|
);
|
||||||
|
return stream(
|
||||||
|
chatPath,
|
||||||
|
requestPayload,
|
||||||
|
getHeaders(),
|
||||||
|
tools as any,
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: {
|
||||||
|
content: string;
|
||||||
|
tool_calls: ChatMessageTool[];
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||||
|
if (tool_calls?.length > 0) {
|
||||||
|
const index = tool_calls[0]?.index;
|
||||||
|
const id = tool_calls[0]?.id;
|
||||||
|
const args = tool_calls[0]?.function?.arguments;
|
||||||
|
if (id) {
|
||||||
|
runTools.push({
|
||||||
|
id,
|
||||||
|
type: tool_calls[0]?.type,
|
||||||
|
function: {
|
||||||
|
name: tool_calls[0]?.function?.name as string,
|
||||||
|
arguments: args,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
runTools[index]["function"]["arguments"] += args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return choices[0]?.delta?.content;
|
||||||
|
},
|
||||||
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
|
(
|
||||||
|
requestPayload: RequestPayload,
|
||||||
|
toolCallMessage: any,
|
||||||
|
toolCallResult: any[],
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.splice(
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.length,
|
||||||
|
0,
|
||||||
|
toolCallMessage,
|
||||||
|
...toolCallResult,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const res = await fetch(chatPath, chatPayload);
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = this.extractMessage(resJson);
|
||||||
|
options.onFinish(message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Request] failed to make a chat request", e);
|
||||||
|
options.onError?.(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async usage() {
|
||||||
|
return {
|
||||||
|
used: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async models(): Promise<LLMModel[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,6 @@ export function useCommand(commands: Commands = {}) {
|
|||||||
interface ChatCommands {
|
interface ChatCommands {
|
||||||
new?: Command;
|
new?: Command;
|
||||||
newm?: Command;
|
newm?: Command;
|
||||||
copy?: Command;
|
|
||||||
next?: Command;
|
next?: Command;
|
||||||
prev?: Command;
|
prev?: Command;
|
||||||
clear?: Command;
|
clear?: Command;
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ import {
|
|||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
isDalle3,
|
isDalle3,
|
||||||
removeOutdatedEntries,
|
|
||||||
showPlugins,
|
showPlugins,
|
||||||
safeLocalStorage,
|
safeLocalStorage,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
@@ -988,7 +987,6 @@ function _Chat() {
|
|||||||
const chatCommands = useChatCommand({
|
const chatCommands = useChatCommand({
|
||||||
new: () => chatStore.newSession(),
|
new: () => chatStore.newSession(),
|
||||||
newm: () => navigate(Path.NewChat),
|
newm: () => navigate(Path.NewChat),
|
||||||
copy: () => chatStore.copySession(),
|
|
||||||
prev: () => chatStore.nextSession(-1),
|
prev: () => chatStore.nextSession(-1),
|
||||||
next: () => chatStore.nextSession(1),
|
next: () => chatStore.nextSession(1),
|
||||||
clear: () =>
|
clear: () =>
|
||||||
@@ -1120,20 +1118,10 @@ function _Chat() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteMessage = (msgId?: string) => {
|
const deleteMessage = (msgId?: string) => {
|
||||||
chatStore.updateCurrentSession((session) => {
|
chatStore.updateCurrentSession(
|
||||||
session.deletedMessageIds &&
|
(session) =>
|
||||||
removeOutdatedEntries(session.deletedMessageIds);
|
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
||||||
session.messages = session.messages.filter((m) => {
|
);
|
||||||
if (m.id !== msgId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!session.deletedMessageIds) {
|
|
||||||
session.deletedMessageIds = {} as Record<string, number>;
|
|
||||||
}
|
|
||||||
session.deletedMessageIds[m.id] = Date.now();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = (msgId: string) => {
|
const onDelete = (msgId: string) => {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import {
|
|||||||
Stability,
|
Stability,
|
||||||
Iflytek,
|
Iflytek,
|
||||||
SAAS_CHAT_URL,
|
SAAS_CHAT_URL,
|
||||||
|
ChatGLM,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
@@ -361,21 +362,6 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
|||||||
</select>
|
</select>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
|
||||||
title={Locale.Settings.Sync.Config.EnableAutoSync.Title}
|
|
||||||
subTitle={Locale.Settings.Sync.Config.EnableAutoSync.SubTitle}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={syncStore.enableAutoSync}
|
|
||||||
onChange={(e) => {
|
|
||||||
syncStore.update(
|
|
||||||
(config) => (config.enableAutoSync = e.currentTarget.checked),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
></input>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Sync.Config.Proxy.Title}
|
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||||
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||||
@@ -1249,6 +1235,47 @@ export function Settings() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chatglmConfigComponent = accessStore.provider ===
|
||||||
|
ServiceProvider.ChatGLM && (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.ChatGLM.Endpoint.Title}
|
||||||
|
subTitle={
|
||||||
|
Locale.Settings.Access.ChatGLM.Endpoint.SubTitle +
|
||||||
|
ChatGLM.ExampleEndpoint
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Settings.Access.ChatGLM.Endpoint.Title}
|
||||||
|
type="text"
|
||||||
|
value={accessStore.chatglmUrl}
|
||||||
|
placeholder={ChatGLM.ExampleEndpoint}
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.chatglmUrl = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.ChatGLM.ApiKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.ChatGLM.ApiKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.ChatGLM.ApiKey.Title}
|
||||||
|
value={accessStore.chatglmApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.ChatGLM.ApiKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.chatglmApiKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const stabilityConfigComponent = accessStore.provider ===
|
const stabilityConfigComponent = accessStore.provider ===
|
||||||
ServiceProvider.Stability && (
|
ServiceProvider.Stability && (
|
||||||
<>
|
<>
|
||||||
@@ -1708,6 +1735,7 @@ export function Settings() {
|
|||||||
{stabilityConfigComponent}
|
{stabilityConfigComponent}
|
||||||
{lflytekConfigComponent}
|
{lflytekConfigComponent}
|
||||||
{XAIConfigComponent}
|
{XAIConfigComponent}
|
||||||
|
{chatglmConfigComponent}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ declare global {
|
|||||||
XAI_URL?: string;
|
XAI_URL?: string;
|
||||||
XAI_API_KEY?: string;
|
XAI_API_KEY?: string;
|
||||||
|
|
||||||
|
// chatglm only
|
||||||
|
CHATGLM_URL?: string;
|
||||||
|
CHATGLM_API_KEY?: string;
|
||||||
|
|
||||||
// custom template for preprocessing user input
|
// custom template for preprocessing user input
|
||||||
DEFAULT_INPUT_TEMPLATE?: string;
|
DEFAULT_INPUT_TEMPLATE?: string;
|
||||||
}
|
}
|
||||||
@@ -151,6 +155,7 @@ export const getServerSideConfig = () => {
|
|||||||
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
||||||
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
||||||
const isXAI = !!process.env.XAI_API_KEY;
|
const isXAI = !!process.env.XAI_API_KEY;
|
||||||
|
const isChatGLM = !!process.env.CHATGLM_API_KEY;
|
||||||
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||||
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||||
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||||
@@ -217,6 +222,10 @@ export const getServerSideConfig = () => {
|
|||||||
xaiUrl: process.env.XAI_URL,
|
xaiUrl: process.env.XAI_URL,
|
||||||
xaiApiKey: getApiKey(process.env.XAI_API_KEY),
|
xaiApiKey: getApiKey(process.env.XAI_API_KEY),
|
||||||
|
|
||||||
|
isChatGLM,
|
||||||
|
chatglmUrl: process.env.CHATGLM_URL,
|
||||||
|
chatglmApiKey: getApiKey(process.env.CHATGLM_API_KEY),
|
||||||
|
|
||||||
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||||
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
|||||||
|
|
||||||
export const XAI_BASE_URL = "https://api.x.ai";
|
export const XAI_BASE_URL = "https://api.x.ai";
|
||||||
|
|
||||||
|
export const CHATGLM_BASE_URL = "https://open.bigmodel.cn";
|
||||||
|
|
||||||
export const CACHE_URL_PREFIX = "/api/cache";
|
export const CACHE_URL_PREFIX = "/api/cache";
|
||||||
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ export enum ApiPath {
|
|||||||
Stability = "/api/stability",
|
Stability = "/api/stability",
|
||||||
Artifacts = "/api/artifacts",
|
Artifacts = "/api/artifacts",
|
||||||
XAI = "/api/xai",
|
XAI = "/api/xai",
|
||||||
|
ChatGLM = "/api/chatglm",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
@@ -115,6 +118,7 @@ export enum ServiceProvider {
|
|||||||
Stability = "Stability",
|
Stability = "Stability",
|
||||||
Iflytek = "Iflytek",
|
Iflytek = "Iflytek",
|
||||||
XAI = "XAI",
|
XAI = "XAI",
|
||||||
|
ChatGLM = "ChatGLM",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||||
@@ -138,6 +142,7 @@ export enum ModelProvider {
|
|||||||
Moonshot = "Moonshot",
|
Moonshot = "Moonshot",
|
||||||
Iflytek = "Iflytek",
|
Iflytek = "Iflytek",
|
||||||
XAI = "XAI",
|
XAI = "XAI",
|
||||||
|
ChatGLM = "ChatGLM",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Stability = {
|
export const Stability = {
|
||||||
@@ -225,6 +230,11 @@ export const XAI = {
|
|||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ChatGLM = {
|
||||||
|
ExampleEndpoint: CHATGLM_BASE_URL,
|
||||||
|
ChatPath: "/api/paas/v4/chat/completions",
|
||||||
|
};
|
||||||
|
|
||||||
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 {{ServiceProvider}}.
|
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||||
@@ -320,6 +330,8 @@ const anthropicModels = [
|
|||||||
"claude-3-haiku-20240307",
|
"claude-3-haiku-20240307",
|
||||||
"claude-3-5-sonnet-20240620",
|
"claude-3-5-sonnet-20240620",
|
||||||
"claude-3-5-sonnet-20241022",
|
"claude-3-5-sonnet-20241022",
|
||||||
|
"claude-3-5-sonnet-latest",
|
||||||
|
"claude-3-opus-latest",
|
||||||
];
|
];
|
||||||
|
|
||||||
const baiduModels = [
|
const baiduModels = [
|
||||||
@@ -377,6 +389,17 @@ const iflytekModels = [
|
|||||||
|
|
||||||
const xAIModes = ["grok-beta"];
|
const xAIModes = ["grok-beta"];
|
||||||
|
|
||||||
|
const chatglmModels = [
|
||||||
|
"glm-4-plus",
|
||||||
|
"glm-4-0520",
|
||||||
|
"glm-4",
|
||||||
|
"glm-4-air",
|
||||||
|
"glm-4-airx",
|
||||||
|
"glm-4-long",
|
||||||
|
"glm-4-flashx",
|
||||||
|
"glm-4-flash",
|
||||||
|
];
|
||||||
|
|
||||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||||
export const DEFAULT_MODELS = [
|
export const DEFAULT_MODELS = [
|
||||||
...openaiModels.map((name) => ({
|
...openaiModels.map((name) => ({
|
||||||
@@ -500,6 +523,17 @@ export const DEFAULT_MODELS = [
|
|||||||
sorted: 11,
|
sorted: 11,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
...chatglmModels.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "chatglm",
|
||||||
|
providerName: "ChatGLM",
|
||||||
|
providerType: "chatglm",
|
||||||
|
sorted: 12,
|
||||||
|
},
|
||||||
|
})),
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const CHAT_PAGE_SIZE = 15;
|
export const CHAT_PAGE_SIZE = 15;
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ const cn = {
|
|||||||
Commands: {
|
Commands: {
|
||||||
new: "新建聊天",
|
new: "新建聊天",
|
||||||
newm: "从面具新建聊天",
|
newm: "从面具新建聊天",
|
||||||
copy: "复制当前聊天",
|
|
||||||
next: "下一个聊天",
|
next: "下一个聊天",
|
||||||
prev: "上一个聊天",
|
prev: "上一个聊天",
|
||||||
clear: "清除上下文",
|
clear: "清除上下文",
|
||||||
@@ -235,10 +234,6 @@ const cn = {
|
|||||||
Title: "同步类型",
|
Title: "同步类型",
|
||||||
SubTitle: "选择喜爱的同步服务器",
|
SubTitle: "选择喜爱的同步服务器",
|
||||||
},
|
},
|
||||||
EnableAutoSync: {
|
|
||||||
Title: "自动同步设置",
|
|
||||||
SubTitle: "在回复完成或删除消息后自动同步数据",
|
|
||||||
},
|
|
||||||
Proxy: {
|
Proxy: {
|
||||||
Title: "启用代理",
|
Title: "启用代理",
|
||||||
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||||
@@ -478,6 +473,17 @@ const cn = {
|
|||||||
SubTitle: "样例:",
|
SubTitle: "样例:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ChatGLM: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义 ChatGLM API Key",
|
||||||
|
Placeholder: "ChatGLM API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
},
|
||||||
Stability: {
|
Stability: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "接口密钥",
|
Title: "接口密钥",
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const en: LocaleType = {
|
|||||||
Commands: {
|
Commands: {
|
||||||
new: "Start a new chat",
|
new: "Start a new chat",
|
||||||
newm: "Start a new chat with mask",
|
newm: "Start a new chat with mask",
|
||||||
copy: "Copy the current Chat",
|
|
||||||
next: "Next Chat",
|
next: "Next Chat",
|
||||||
prev: "Previous Chat",
|
prev: "Previous Chat",
|
||||||
clear: "Clear Context",
|
clear: "Clear Context",
|
||||||
@@ -237,11 +236,6 @@ const en: LocaleType = {
|
|||||||
Title: "Sync Type",
|
Title: "Sync Type",
|
||||||
SubTitle: "Choose your favorite sync service",
|
SubTitle: "Choose your favorite sync service",
|
||||||
},
|
},
|
||||||
EnableAutoSync: {
|
|
||||||
Title: "Auto Sync Settings",
|
|
||||||
SubTitle:
|
|
||||||
"Automatically synchronize data after replying or deleting messages",
|
|
||||||
},
|
|
||||||
Proxy: {
|
Proxy: {
|
||||||
Title: "Enable CORS Proxy",
|
Title: "Enable CORS Proxy",
|
||||||
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
||||||
@@ -463,6 +457,17 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Example: ",
|
SubTitle: "Example: ",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ChatGLM: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "ChatGLM API Key",
|
||||||
|
SubTitle: "Use a custom ChatGLM API Key",
|
||||||
|
Placeholder: "ChatGLM API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
Stability: {
|
Stability: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "Stability API Key",
|
Title: "Stability API Key",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
STABILITY_BASE_URL,
|
STABILITY_BASE_URL,
|
||||||
IFLYTEK_BASE_URL,
|
IFLYTEK_BASE_URL,
|
||||||
XAI_BASE_URL,
|
XAI_BASE_URL,
|
||||||
|
CHATGLM_BASE_URL,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { getHeaders } from "../client/api";
|
import { getHeaders } from "../client/api";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
@@ -47,6 +48,8 @@ const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
|||||||
|
|
||||||
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||||
|
|
||||||
|
const DEFAULT_CHATGLM_URL = isApp ? CHATGLM_BASE_URL : ApiPath.ChatGLM;
|
||||||
|
|
||||||
const DEFAULT_ACCESS_STATE = {
|
const DEFAULT_ACCESS_STATE = {
|
||||||
accessCode: "",
|
accessCode: "",
|
||||||
useCustomConfig: false,
|
useCustomConfig: false,
|
||||||
@@ -108,6 +111,10 @@ const DEFAULT_ACCESS_STATE = {
|
|||||||
xaiUrl: DEFAULT_XAI_URL,
|
xaiUrl: DEFAULT_XAI_URL,
|
||||||
xaiApiKey: "",
|
xaiApiKey: "",
|
||||||
|
|
||||||
|
// chatglm
|
||||||
|
chatglmUrl: DEFAULT_CHATGLM_URL,
|
||||||
|
chatglmApiKey: "",
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
@@ -180,6 +187,10 @@ export const useAccessStore = createPersistStore(
|
|||||||
return ensure(get(), ["xaiApiKey"]);
|
return ensure(get(), ["xaiApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidChatGLM() {
|
||||||
|
return ensure(get(), ["chatglmApiKey"]);
|
||||||
|
},
|
||||||
|
|
||||||
isAuthorized() {
|
isAuthorized() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
|
|
||||||
@@ -196,6 +207,7 @@ export const useAccessStore = createPersistStore(
|
|||||||
this.isValidMoonshot() ||
|
this.isValidMoonshot() ||
|
||||||
this.isValidIflytek() ||
|
this.isValidIflytek() ||
|
||||||
this.isValidXAI() ||
|
this.isValidXAI() ||
|
||||||
|
this.isValidChatGLM() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||||
);
|
);
|
||||||
@@ -223,7 +235,7 @@ 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(() => ({ lastUpdateTime: Date.now(), ...res }));
|
set(() => ({ ...res }));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error("[Config] failed to fetch config");
|
console.error("[Config] failed to fetch config");
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { getMessageTextContent, trimTopic } from "../utils";
|
||||||
getMessageTextContent,
|
|
||||||
trimTopic,
|
|
||||||
removeOutdatedEntries,
|
|
||||||
} from "../utils";
|
|
||||||
|
|
||||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@@ -33,7 +29,6 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
|
|||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { createEmptyMask, Mask } from "./mask";
|
import { createEmptyMask, Mask } from "./mask";
|
||||||
import { useSyncStore } from "./sync";
|
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
@@ -85,7 +80,6 @@ export interface ChatSession {
|
|||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
lastSummarizeIndex: number;
|
lastSummarizeIndex: number;
|
||||||
clearContextIndex?: number;
|
clearContextIndex?: number;
|
||||||
deletedMessageIds?: Record<string, number>;
|
|
||||||
|
|
||||||
mask: Mask;
|
mask: Mask;
|
||||||
}
|
}
|
||||||
@@ -109,7 +103,6 @@ function createEmptySession(): ChatSession {
|
|||||||
},
|
},
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
lastSummarizeIndex: 0,
|
lastSummarizeIndex: 0,
|
||||||
deletedMessageIds: {},
|
|
||||||
|
|
||||||
mask: createEmptyMask(),
|
mask: createEmptyMask(),
|
||||||
};
|
};
|
||||||
@@ -195,19 +188,9 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cloudSyncTimer: any = null;
|
|
||||||
function noticeCloudSync(): void {
|
|
||||||
const syncStore = useSyncStore.getState();
|
|
||||||
cloudSyncTimer && clearTimeout(cloudSyncTimer);
|
|
||||||
cloudSyncTimer = setTimeout(() => {
|
|
||||||
syncStore.autoSync();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_CHAT_STATE = {
|
const DEFAULT_CHAT_STATE = {
|
||||||
sessions: [createEmptySession()],
|
sessions: [createEmptySession()],
|
||||||
currentSessionIndex: 0,
|
currentSessionIndex: 0,
|
||||||
deletedSessionIds: {} as Record<string, number>,
|
|
||||||
lastInput: "",
|
lastInput: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,28 +240,6 @@ export const useChatStore = createPersistStore(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
copySession() {
|
|
||||||
set((state) => {
|
|
||||||
const { sessions, currentSessionIndex } = state;
|
|
||||||
const emptySession = createEmptySession();
|
|
||||||
|
|
||||||
// copy the session
|
|
||||||
const curSession = JSON.parse(
|
|
||||||
JSON.stringify(sessions[currentSessionIndex]),
|
|
||||||
);
|
|
||||||
curSession.id = emptySession.id;
|
|
||||||
curSession.lastUpdate = emptySession.lastUpdate;
|
|
||||||
|
|
||||||
const newSessions = [...sessions];
|
|
||||||
newSessions.splice(0, 0, curSession);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentSessionIndex: 0,
|
|
||||||
sessions: newSessions,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
moveSession(from: number, to: number) {
|
moveSession(from: number, to: number) {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const { sessions, currentSessionIndex: oldIndex } = state;
|
const { sessions, currentSessionIndex: oldIndex } = state;
|
||||||
@@ -341,18 +302,7 @@ export const useChatStore = createPersistStore(
|
|||||||
if (!deletedSession) return;
|
if (!deletedSession) return;
|
||||||
|
|
||||||
const sessions = get().sessions.slice();
|
const sessions = get().sessions.slice();
|
||||||
const deletedSessionIds = { ...get().deletedSessionIds };
|
sessions.splice(index, 1);
|
||||||
|
|
||||||
removeOutdatedEntries(deletedSessionIds);
|
|
||||||
|
|
||||||
const hasDelSessions = sessions.splice(index, 1);
|
|
||||||
if (hasDelSessions?.length) {
|
|
||||||
hasDelSessions.forEach((session) => {
|
|
||||||
if (session.messages.length > 0) {
|
|
||||||
deletedSessionIds[session.id] = Date.now();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentIndex = get().currentSessionIndex;
|
const currentIndex = get().currentSessionIndex;
|
||||||
let nextIndex = Math.min(
|
let nextIndex = Math.min(
|
||||||
@@ -369,24 +319,19 @@ export const useChatStore = createPersistStore(
|
|||||||
const restoreState = {
|
const restoreState = {
|
||||||
currentSessionIndex: get().currentSessionIndex,
|
currentSessionIndex: get().currentSessionIndex,
|
||||||
sessions: get().sessions.slice(),
|
sessions: get().sessions.slice(),
|
||||||
deletedSessionIds: get().deletedSessionIds,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
set(() => ({
|
set(() => ({
|
||||||
currentSessionIndex: nextIndex,
|
currentSessionIndex: nextIndex,
|
||||||
sessions,
|
sessions,
|
||||||
deletedSessionIds,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
noticeCloudSync();
|
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
Locale.Home.DeleteToast,
|
Locale.Home.DeleteToast,
|
||||||
{
|
{
|
||||||
text: Locale.Home.Revert,
|
text: Locale.Home.Revert,
|
||||||
onClick() {
|
onClick() {
|
||||||
set(() => restoreState);
|
set(() => restoreState);
|
||||||
noticeCloudSync();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
@@ -407,24 +352,6 @@ export const useChatStore = createPersistStore(
|
|||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|
||||||
sortSessions() {
|
|
||||||
const currentSession = get().currentSession();
|
|
||||||
const sessions = get().sessions.slice();
|
|
||||||
|
|
||||||
sessions.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
|
||||||
);
|
|
||||||
const currentSessionIndex = sessions.findIndex((session) => {
|
|
||||||
return session && currentSession && session.id === currentSession.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
set((state) => ({
|
|
||||||
currentSessionIndex,
|
|
||||||
sessions,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onNewMessage(message: ChatMessage) {
|
onNewMessage(message: ChatMessage) {
|
||||||
get().updateCurrentSession((session) => {
|
get().updateCurrentSession((session) => {
|
||||||
session.messages = session.messages.concat();
|
session.messages = session.messages.concat();
|
||||||
@@ -432,8 +359,6 @@ export const useChatStore = createPersistStore(
|
|||||||
});
|
});
|
||||||
get().updateStat(message);
|
get().updateStat(message);
|
||||||
get().summarizeSession();
|
get().summarizeSession();
|
||||||
get().sortSessions();
|
|
||||||
noticeCloudSync();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async onUserInput(content: string, attachImages?: string[]) {
|
async onUserInput(content: string, attachImages?: string[]) {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
|||||||
|
|
||||||
const DEFAULT_SYNC_STATE = {
|
const DEFAULT_SYNC_STATE = {
|
||||||
provider: ProviderType.WebDAV,
|
provider: ProviderType.WebDAV,
|
||||||
enableAutoSync: true,
|
|
||||||
useProxy: true,
|
useProxy: true,
|
||||||
proxyUrl: ApiPath.Cors as string,
|
proxyUrl: ApiPath.Cors as string,
|
||||||
|
|
||||||
@@ -44,8 +43,6 @@ const DEFAULT_SYNC_STATE = {
|
|||||||
lastProvider: "",
|
lastProvider: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastSyncTime = 0;
|
|
||||||
|
|
||||||
export const useSyncStore = createPersistStore(
|
export const useSyncStore = createPersistStore(
|
||||||
DEFAULT_SYNC_STATE,
|
DEFAULT_SYNC_STATE,
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
@@ -92,16 +89,6 @@ export const useSyncStore = createPersistStore(
|
|||||||
},
|
},
|
||||||
|
|
||||||
async sync() {
|
async sync() {
|
||||||
if (lastSyncTime && lastSyncTime >= Date.now() - 800) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastSyncTime = Date.now();
|
|
||||||
|
|
||||||
const enableAutoSync = get().enableAutoSync;
|
|
||||||
if (!enableAutoSync) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localState = getLocalAppState();
|
const localState = getLocalAppState();
|
||||||
const provider = get().provider;
|
const provider = get().provider;
|
||||||
const config = get()[provider];
|
const config = get()[provider];
|
||||||
@@ -116,7 +103,9 @@ export const useSyncStore = createPersistStore(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
const parsedRemoteState = JSON.parse(remoteState) as AppState;
|
const parsedRemoteState = JSON.parse(
|
||||||
|
await client.get(config.username),
|
||||||
|
) as AppState;
|
||||||
mergeAppState(localState, parsedRemoteState);
|
mergeAppState(localState, parsedRemoteState);
|
||||||
setLocalAppState(localState);
|
setLocalAppState(localState);
|
||||||
}
|
}
|
||||||
@@ -134,14 +123,6 @@ export const useSyncStore = createPersistStore(
|
|||||||
const client = this.getClient();
|
const client = this.getClient();
|
||||||
return await client.check();
|
return await client.check();
|
||||||
},
|
},
|
||||||
|
|
||||||
async autoSync() {
|
|
||||||
const { lastSyncTime, provider } = get();
|
|
||||||
const syncStore = useSyncStore.getState();
|
|
||||||
if (lastSyncTime && syncStore.cloudSync()) {
|
|
||||||
syncStore.sync();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Sync,
|
name: StoreKey.Sync,
|
||||||
|
|||||||
16
app/utils.ts
16
app/utils.ts
@@ -274,24 +274,12 @@ export function isDalle3(model: string) {
|
|||||||
return "dall-e-3" === model;
|
return "dall-e-3" === model;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeOutdatedEntries(
|
|
||||||
timeMap: Record<string, number>,
|
|
||||||
): Record<string, number> {
|
|
||||||
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
||||||
// Delete data from a month ago
|
|
||||||
Object.keys(timeMap).forEach((id) => {
|
|
||||||
if (timeMap[id] < oneMonthAgo) {
|
|
||||||
delete timeMap[id];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return timeMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showPlugins(provider: ServiceProvider, model: string) {
|
export function showPlugins(provider: ServiceProvider, model: string) {
|
||||||
if (
|
if (
|
||||||
provider == ServiceProvider.OpenAI ||
|
provider == ServiceProvider.OpenAI ||
|
||||||
provider == ServiceProvider.Azure ||
|
provider == ServiceProvider.Azure ||
|
||||||
provider == ServiceProvider.Moonshot
|
provider == ServiceProvider.Moonshot ||
|
||||||
|
provider == ServiceProvider.ChatGLM
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useMaskStore } from "../store/mask";
|
|||||||
import { usePromptStore } from "../store/prompt";
|
import { usePromptStore } from "../store/prompt";
|
||||||
import { StoreKey } from "../constant";
|
import { StoreKey } from "../constant";
|
||||||
import { merge } from "./merge";
|
import { merge } from "./merge";
|
||||||
import { removeOutdatedEntries } from "@/app/utils";
|
|
||||||
|
|
||||||
type NonFunctionKeys<T> = {
|
type NonFunctionKeys<T> = {
|
||||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
||||||
@@ -66,10 +65,7 @@ type StateMerger = {
|
|||||||
const MergeStates: StateMerger = {
|
const MergeStates: StateMerger = {
|
||||||
[StoreKey.Chat]: (localState, remoteState) => {
|
[StoreKey.Chat]: (localState, remoteState) => {
|
||||||
// merge sessions
|
// merge sessions
|
||||||
const currentSession = useChatStore.getState().currentSession();
|
|
||||||
|
|
||||||
const localSessions: Record<string, ChatSession> = {};
|
const localSessions: Record<string, ChatSession> = {};
|
||||||
const localDeletedSessionIds = localState.deletedSessionIds || {};
|
|
||||||
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||||
|
|
||||||
remoteState.sessions.forEach((remoteSession) => {
|
remoteState.sessions.forEach((remoteSession) => {
|
||||||
@@ -79,98 +75,29 @@ const MergeStates: StateMerger = {
|
|||||||
const localSession = localSessions[remoteSession.id];
|
const localSession = localSessions[remoteSession.id];
|
||||||
if (!localSession) {
|
if (!localSession) {
|
||||||
// if remote session is new, just merge it
|
// if remote session is new, just merge it
|
||||||
if (
|
localState.sessions.push(remoteSession);
|
||||||
(localDeletedSessionIds[remoteSession.id] || -1) <
|
|
||||||
remoteSession.lastUpdate
|
|
||||||
) {
|
|
||||||
localState.sessions.push(remoteSession);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// if both have the same session id, merge the messages
|
// if both have the same session id, merge the messages
|
||||||
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
||||||
const localDeletedMessageIds = localSession.deletedMessageIds || {};
|
|
||||||
remoteSession.messages.forEach((m) => {
|
remoteSession.messages.forEach((m) => {
|
||||||
if (!localMessageIds.has(m.id)) {
|
if (!localMessageIds.has(m.id)) {
|
||||||
if (
|
localSession.messages.push(m);
|
||||||
!localDeletedMessageIds[m.id] ||
|
|
||||||
new Date(localDeletedMessageIds[m.id]).toLocaleString() < m.date
|
|
||||||
) {
|
|
||||||
localSession.messages.push(m);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const remoteDeletedMessageIds = remoteSession.deletedMessageIds || {};
|
|
||||||
localSession.messages = localSession.messages.filter((localMessage) => {
|
|
||||||
return (
|
|
||||||
!remoteDeletedMessageIds[localMessage.id] ||
|
|
||||||
new Date(localDeletedMessageIds[localMessage.id]).toLocaleString() <
|
|
||||||
localMessage.date
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// sort local messages with date field in asc order
|
// sort local messages with date field in asc order
|
||||||
localSession.messages.sort(
|
localSession.messages.sort(
|
||||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||||
);
|
);
|
||||||
localSession.lastUpdate = Math.max(
|
|
||||||
remoteSession.lastUpdate,
|
|
||||||
localSession.lastUpdate,
|
|
||||||
);
|
|
||||||
|
|
||||||
const deletedMessageIds = {
|
|
||||||
...remoteDeletedMessageIds,
|
|
||||||
...localDeletedMessageIds,
|
|
||||||
};
|
|
||||||
removeOutdatedEntries(deletedMessageIds);
|
|
||||||
localSession.deletedMessageIds = deletedMessageIds;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const remoteDeletedSessionIds = remoteState.deletedSessionIds || {};
|
|
||||||
|
|
||||||
const finalIds: Record<string, any> = {};
|
|
||||||
localState.sessions = localState.sessions.filter((localSession) => {
|
|
||||||
// 去除掉重复的会话
|
|
||||||
if (finalIds[localSession.id]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
finalIds[localSession.id] = true;
|
|
||||||
|
|
||||||
// 去除掉非首个空会话,避免多个空会话在中间,不方便管理
|
|
||||||
if (
|
|
||||||
localSession.messages.length === 0 &&
|
|
||||||
localSession != localState.sessions[0]
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 去除云端删除并且删除时间小于本地修改时间的会话
|
|
||||||
return (
|
|
||||||
(remoteDeletedSessionIds[localSession.id] || -1) <=
|
|
||||||
localSession.lastUpdate
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// sort local sessions with date field in desc order
|
// sort local sessions with date field in desc order
|
||||||
localState.sessions.sort(
|
localState.sessions.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const deletedSessionIds = {
|
|
||||||
...remoteDeletedSessionIds,
|
|
||||||
...localDeletedSessionIds,
|
|
||||||
};
|
|
||||||
removeOutdatedEntries(deletedSessionIds);
|
|
||||||
localState.deletedSessionIds = deletedSessionIds;
|
|
||||||
|
|
||||||
localState.currentSessionIndex = localState.sessions.findIndex(
|
|
||||||
(session) => {
|
|
||||||
return session && currentSession && session.id === currentSession.id;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return localState;
|
return localState;
|
||||||
},
|
},
|
||||||
[StoreKey.Prompt]: (localState, remoteState) => {
|
[StoreKey.Prompt]: (localState, remoteState) => {
|
||||||
@@ -226,9 +153,9 @@ export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
|
|||||||
remoteState: T,
|
remoteState: T,
|
||||||
) {
|
) {
|
||||||
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
||||||
const remoteUpdateTime = remoteState.lastUpdateTime ?? 1;
|
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
|
||||||
|
|
||||||
if (localUpdateTime >= remoteUpdateTime) {
|
if (localUpdateTime < remoteUpdateTime) {
|
||||||
merge(remoteState, localState);
|
merge(remoteState, localState);
|
||||||
return { ...remoteState };
|
return { ...remoteState };
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
29
docs/bt-cn.md
Normal file
29
docs/bt-cn.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 宝塔面板 的部署说明
|
||||||
|
|
||||||
|
## 拥有自己的宝塔
|
||||||
|
当你需要通过 宝塔面板 部署本项目之前,需要在服务器上先安装好 宝塔面板工具。 接下来的 部署流程 都建立在已有宝塔面板的前提下。宝塔安装请参考 ([宝塔官网](https://www.bt.cn/new/download.html))
|
||||||
|
|
||||||
|
> 注意:本项目需要宝塔面板版本 9.2.0 及以上
|
||||||
|
|
||||||
|
## 一键安装
|
||||||
|

|
||||||
|
1. 在 宝塔面板 -> Docker -> 应用商店 页面,搜索 ChatGPT-Next-Web 找到本项目的docker应用;
|
||||||
|
2. 点击 安装 开始部署本项目
|
||||||
|
|
||||||
|

|
||||||
|
1. 在项目配置页,根据要求开始配置环境变量;
|
||||||
|
2. 如勾选 允许外部访问 配置,请注意为配置的 web端口 开放安全组端口访问权限;
|
||||||
|
3. 请确保你添加了正确的 Open Api Key,否则无法使用;当配置 OpenAI官方 提供的key(国内无法访问),请配置代理地址;
|
||||||
|
4. 建议配置 访问权限密码,否则部署后所有人均可使用已配置的 Open Api Key(当允许外部访问时);
|
||||||
|
5. 点击 确认 开始自动部署。
|
||||||
|
|
||||||
|
## 如何访问
|
||||||
|

|
||||||
|
通过根据 服务器IP地址 和配置的 web端口 http://$(host):$(port),在浏览器中打开 ChatGPT-Next-Web。
|
||||||
|
|
||||||
|

|
||||||
|
若配置了 访问权限密码,访问大模型前需要登录,请点击 登录,获取访问权限。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
BIN
docs/images/bt/bt-install-1.jpeg
Normal file
BIN
docs/images/bt/bt-install-1.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
BIN
docs/images/bt/bt-install-2.jpeg
Normal file
BIN
docs/images/bt/bt-install-2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
BIN
docs/images/bt/bt-install-3.jpeg
Normal file
BIN
docs/images/bt/bt-install-3.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
BIN
docs/images/bt/bt-install-4.jpeg
Normal file
BIN
docs/images/bt/bt-install-4.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
BIN
docs/images/bt/bt-install-5.jpeg
Normal file
BIN
docs/images/bt/bt-install-5.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
docs/images/bt/bt-install-6.jpeg
Normal file
BIN
docs/images/bt/bt-install-6.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Reference in New Issue
Block a user