mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-03-04 03:04:27 +08:00
Compare commits
73 Commits
feature/aa
...
3e02a71e0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e02a71e0d | ||
|
|
4f49626303 | ||
|
|
45db20c1c3 | ||
|
|
82994843f5 | ||
|
|
1110a087a0 | ||
|
|
f0b3e10a6c | ||
|
|
f89872b833 | ||
|
|
90ced92876 | ||
|
|
2c74559010 | ||
|
|
e3ca7e8b44 | ||
|
|
4745706c42 | ||
|
|
c7c2c0211a | ||
|
|
65bb962fc0 | ||
|
|
e791cd441d | ||
|
|
8455fefc8a | ||
|
|
06f897f32f | ||
|
|
deb1e76c41 | ||
|
|
463fa743e9 | ||
|
|
f80da8a263 | ||
|
|
98ab561607 | ||
|
|
9a025ae196 | ||
|
|
c4ae73d8a1 | ||
|
|
31900cbff3 | ||
|
|
c6657d3d0c | ||
|
|
cf7c6f2b9a | ||
|
|
41242cae9b | ||
|
|
89edebd93c | ||
|
|
60bd3c56c1 | ||
|
|
659a389fd4 | ||
|
|
144fdc9b7c | ||
|
|
35f52886c4 | ||
|
|
9551f5dfc6 | ||
|
|
370ce3eeca | ||
|
|
5ae4921ee0 | ||
|
|
6f3d7530b9 | ||
|
|
6dc868154d | ||
|
|
ccacfec918 | ||
|
|
c204031ea7 | ||
|
|
2bf72d0324 | ||
|
|
e8c7ac0c45 | ||
|
|
0638db146e | ||
|
|
2d68f179d7 | ||
|
|
f1d69cb312 | ||
|
|
31baa10363 | ||
|
|
2fdb35bcc8 | ||
|
|
5c51fd2ed8 | ||
|
|
d0b7ddc1d6 | ||
|
|
0745b6498d | ||
|
|
fc97c4b06f | ||
|
|
fdb89af355 | ||
|
|
e515f0f957 | ||
|
|
31f282970b | ||
|
|
b2336f5ed9 | ||
|
|
0a6ddda992 | ||
|
|
5e1064a5c8 | ||
|
|
2ee2d50ae6 | ||
|
|
eae593d660 | ||
|
|
621b1480c2 | ||
|
|
4b22aaf979 | ||
|
|
93bfb55822 | ||
|
|
648e60028d | ||
|
|
4f876f3e65 | ||
|
|
faac0d9817 | ||
|
|
22c79595fb | ||
|
|
5065091b74 | ||
|
|
22f61295bc | ||
|
|
c440637ad0 | ||
|
|
284d33bcdf | ||
|
|
d9573973ca | ||
|
|
cd354cf045 | ||
|
|
1cce87acaa | ||
|
|
78c4084501 | ||
|
|
1d0a40b9e8 |
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: yidadaa/chatgpt-next-web
|
images: ${{ secrets.DOCKER_USERNAME }}/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
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||||
|
|
||||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [<img src="https://img.shields.io/badge/BT_Deploy-Install-20a53a" alt="Open in Gitpod" height="30">](https://www.bt.cn/new/download.html)
|
||||||
|
|
||||||
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { handle as alibabaHandler } from "../../alibaba";
|
|||||||
import { handle as moonshotHandler } from "../../moonshot";
|
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 proxyHandler } from "../../proxy";
|
import { handle as proxyHandler } from "../../proxy";
|
||||||
|
|
||||||
async function handle(
|
async function handle(
|
||||||
@@ -38,6 +39,8 @@ async function handle(
|
|||||||
return stabilityHandler(req, { params });
|
return stabilityHandler(req, { params });
|
||||||
case ApiPath.Iflytek:
|
case ApiPath.Iflytek:
|
||||||
return iflytekHandler(req, { params });
|
return iflytekHandler(req, { params });
|
||||||
|
case ApiPath.XAI:
|
||||||
|
return xaiHandler(req, { params });
|
||||||
case ApiPath.OpenAI:
|
case ApiPath.OpenAI:
|
||||||
return openaiHandler(req, { params });
|
return openaiHandler(req, { params });
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
|||||||
systemApiKey =
|
systemApiKey =
|
||||||
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.XAI:
|
||||||
|
systemApiKey = serverConfig.xaiApiKey;
|
||||||
|
break;
|
||||||
case ModelProvider.GPT:
|
case ModelProvider.GPT:
|
||||||
default:
|
default:
|
||||||
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
||||||
|
|||||||
128
app/api/xai.ts
Normal file
128
app/api/xai.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import {
|
||||||
|
XAI_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("[XAI Route] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.XAI);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(req);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[XAI] ", 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.XAI, "");
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.xaiUrl || XAI_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}`;
|
||||||
|
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.XAI as string,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[XAI] 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { QwenApi } from "./platforms/alibaba";
|
|||||||
import { HunyuanApi } from "./platforms/tencent";
|
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";
|
||||||
|
|
||||||
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];
|
||||||
@@ -152,6 +153,9 @@ export class ClientApi {
|
|||||||
case ModelProvider.Iflytek:
|
case ModelProvider.Iflytek:
|
||||||
this.llm = new SparkApi();
|
this.llm = new SparkApi();
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.XAI:
|
||||||
|
this.llm = new XAIApi();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.llm = new ChatGPTApi();
|
this.llm = new ChatGPTApi();
|
||||||
}
|
}
|
||||||
@@ -239,6 +243,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
||||||
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 isEnabledAccessControl = accessStore.enabledAccessControl();
|
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||||
const apiKey = isGoogle
|
const apiKey = isGoogle
|
||||||
? accessStore.googleApiKey
|
? accessStore.googleApiKey
|
||||||
@@ -252,6 +257,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
? accessStore.alibabaApiKey
|
? accessStore.alibabaApiKey
|
||||||
: isMoonshot
|
: isMoonshot
|
||||||
? accessStore.moonshotApiKey
|
? accessStore.moonshotApiKey
|
||||||
|
: isXAI
|
||||||
|
? accessStore.xaiApiKey
|
||||||
: isIflytek
|
: isIflytek
|
||||||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||||
@@ -266,6 +273,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
|||||||
isAlibaba,
|
isAlibaba,
|
||||||
isMoonshot,
|
isMoonshot,
|
||||||
isIflytek,
|
isIflytek,
|
||||||
|
isXAI,
|
||||||
apiKey,
|
apiKey,
|
||||||
isEnabledAccessControl,
|
isEnabledAccessControl,
|
||||||
};
|
};
|
||||||
@@ -328,6 +336,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
|||||||
return new ClientApi(ModelProvider.Moonshot);
|
return new ClientApi(ModelProvider.Moonshot);
|
||||||
case ServiceProvider.Iflytek:
|
case ServiceProvider.Iflytek:
|
||||||
return new ClientApi(ModelProvider.Iflytek);
|
return new ClientApi(ModelProvider.Iflytek);
|
||||||
|
case ServiceProvider.XAI:
|
||||||
|
return new ClientApi(ModelProvider.XAI);
|
||||||
default:
|
default:
|
||||||
return new ClientApi(ModelProvider.GPT);
|
return new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
|||||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
import { RequestPayload } from "./openai";
|
import { RequestPayload } from "./openai";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export type MultiBlockContent = {
|
export type MultiBlockContent = {
|
||||||
type: "image" | "text";
|
type: "image" | "text";
|
||||||
|
|||||||
@@ -192,7 +192,10 @@ export class GeminiProApi implements LLMApi {
|
|||||||
requestPayload,
|
requestPayload,
|
||||||
getHeaders(),
|
getHeaders(),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
[{ functionDeclarations: tools.map((tool) => tool.function) }],
|
tools.length > 0
|
||||||
|
? // @ts-ignore
|
||||||
|
[{ functionDeclarations: tools.map((tool) => tool.function) }]
|
||||||
|
: [],
|
||||||
funcs,
|
funcs,
|
||||||
controller,
|
controller,
|
||||||
// parseSSE
|
// parseSSE
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
import { RequestPayload } from "./openai";
|
import { RequestPayload } from "./openai";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export class MoonshotApi implements LLMApi {
|
export class MoonshotApi implements LLMApi {
|
||||||
private disableListModels = true;
|
private disableListModels = true;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
isVisionModel,
|
isVisionModel,
|
||||||
isDalle3 as _isDalle3,
|
isDalle3 as _isDalle3,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
|
|||||||
193
app/client/platforms/xai.ts
Normal file
193
app/client/platforms/xai.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
"use client";
|
||||||
|
// azure and openai, using same models. so using same LLMApi.
|
||||||
|
import { ApiPath, XAI_BASE_URL, XAI, 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 XAIApi implements LLMApi {
|
||||||
|
private disableListModels = true;
|
||||||
|
|
||||||
|
path(path: string): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.xaiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
const apiPath = ApiPath.XAI;
|
||||||
|
baseUrl = isApp ? XAI_BASE_URL : apiPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
|
||||||
|
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] xai payload: ", requestPayload);
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatPath = this.path(XAI.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,6 +35,7 @@ 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,6 +70,7 @@ import {
|
|||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
isDalle3,
|
isDalle3,
|
||||||
|
removeOutdatedEntries,
|
||||||
showPlugins,
|
showPlugins,
|
||||||
safeLocalStorage,
|
safeLocalStorage,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
@@ -987,6 +988,7 @@ 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: () =>
|
||||||
@@ -1118,10 +1120,20 @@ function _Chat() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteMessage = (msgId?: string) => {
|
const deleteMessage = (msgId?: string) => {
|
||||||
chatStore.updateCurrentSession(
|
chatStore.updateCurrentSession((session) => {
|
||||||
(session) =>
|
session.deletedMessageIds &&
|
||||||
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
removeOutdatedEntries(session.deletedMessageIds);
|
||||||
);
|
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) => {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
ByteDance,
|
ByteDance,
|
||||||
Alibaba,
|
Alibaba,
|
||||||
Moonshot,
|
Moonshot,
|
||||||
|
XAI,
|
||||||
Google,
|
Google,
|
||||||
GoogleSafetySettingsThreshold,
|
GoogleSafetySettingsThreshold,
|
||||||
OPENAI_BASE_URL,
|
OPENAI_BASE_URL,
|
||||||
@@ -360,6 +361,21 @@ 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}
|
||||||
@@ -1194,6 +1210,45 @@ export function Settings() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
|
||||||
|
<>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||||
|
subTitle={
|
||||||
|
Locale.Settings.Access.XAI.Endpoint.SubTitle + XAI.ExampleEndpoint
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||||
|
type="text"
|
||||||
|
value={accessStore.xaiUrl}
|
||||||
|
placeholder={XAI.ExampleEndpoint}
|
||||||
|
onChange={(e) =>
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.xaiUrl = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||||
|
subTitle={Locale.Settings.Access.XAI.ApiKey.SubTitle}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
aria-label={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||||
|
value={accessStore.xaiApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.XAI.ApiKey.Placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
accessStore.update(
|
||||||
|
(access) => (access.xaiApiKey = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const stabilityConfigComponent = accessStore.provider ===
|
const stabilityConfigComponent = accessStore.provider ===
|
||||||
ServiceProvider.Stability && (
|
ServiceProvider.Stability && (
|
||||||
<>
|
<>
|
||||||
@@ -1652,6 +1707,7 @@ export function Settings() {
|
|||||||
{moonshotConfigComponent}
|
{moonshotConfigComponent}
|
||||||
{stabilityConfigComponent}
|
{stabilityConfigComponent}
|
||||||
{lflytekConfigComponent}
|
{lflytekConfigComponent}
|
||||||
|
{XAIConfigComponent}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ declare global {
|
|||||||
IFLYTEK_API_KEY?: string;
|
IFLYTEK_API_KEY?: string;
|
||||||
IFLYTEK_API_SECRET?: string;
|
IFLYTEK_API_SECRET?: string;
|
||||||
|
|
||||||
|
// xai only
|
||||||
|
XAI_URL?: string;
|
||||||
|
XAI_API_KEY?: string;
|
||||||
|
|
||||||
// custom template for preprocessing user input
|
// custom template for preprocessing user input
|
||||||
DEFAULT_INPUT_TEMPLATE?: string;
|
DEFAULT_INPUT_TEMPLATE?: string;
|
||||||
}
|
}
|
||||||
@@ -146,6 +150,7 @@ export const getServerSideConfig = () => {
|
|||||||
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
||||||
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 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);
|
||||||
@@ -208,6 +213,10 @@ export const getServerSideConfig = () => {
|
|||||||
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
||||||
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
||||||
|
|
||||||
|
isXAI,
|
||||||
|
xaiUrl: process.env.XAI_URL,
|
||||||
|
xaiApiKey: getApiKey(process.env.XAI_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),
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
|||||||
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
||||||
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
||||||
|
|
||||||
|
export const XAI_BASE_URL = "https://api.x.ai";
|
||||||
|
|
||||||
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`;
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ export enum ApiPath {
|
|||||||
Iflytek = "/api/iflytek",
|
Iflytek = "/api/iflytek",
|
||||||
Stability = "/api/stability",
|
Stability = "/api/stability",
|
||||||
Artifacts = "/api/artifacts",
|
Artifacts = "/api/artifacts",
|
||||||
|
XAI = "/api/xai",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
@@ -111,6 +114,7 @@ export enum ServiceProvider {
|
|||||||
Moonshot = "Moonshot",
|
Moonshot = "Moonshot",
|
||||||
Stability = "Stability",
|
Stability = "Stability",
|
||||||
Iflytek = "Iflytek",
|
Iflytek = "Iflytek",
|
||||||
|
XAI = "XAI",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -133,6 +137,7 @@ export enum ModelProvider {
|
|||||||
Hunyuan = "Hunyuan",
|
Hunyuan = "Hunyuan",
|
||||||
Moonshot = "Moonshot",
|
Moonshot = "Moonshot",
|
||||||
Iflytek = "Iflytek",
|
Iflytek = "Iflytek",
|
||||||
|
XAI = "XAI",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Stability = {
|
export const Stability = {
|
||||||
@@ -215,6 +220,11 @@ export const Iflytek = {
|
|||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const XAI = {
|
||||||
|
ExampleEndpoint: XAI_BASE_URL,
|
||||||
|
ChatPath: "v1/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}}.
|
||||||
@@ -364,6 +374,8 @@ const iflytekModels = [
|
|||||||
"4.0Ultra",
|
"4.0Ultra",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const xAIModes = ["grok-beta"];
|
||||||
|
|
||||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||||
export const DEFAULT_MODELS = [
|
export const DEFAULT_MODELS = [
|
||||||
...openaiModels.map((name) => ({
|
...openaiModels.map((name) => ({
|
||||||
@@ -476,6 +488,17 @@ export const DEFAULT_MODELS = [
|
|||||||
sorted: 10,
|
sorted: 10,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
...xAIModes.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "xai",
|
||||||
|
providerName: "XAI",
|
||||||
|
providerType: "xai",
|
||||||
|
sorted: 11,
|
||||||
|
},
|
||||||
|
})),
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const CHAT_PAGE_SIZE = 15;
|
export const CHAT_PAGE_SIZE = 15;
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const cn = {
|
|||||||
Commands: {
|
Commands: {
|
||||||
new: "新建聊天",
|
new: "新建聊天",
|
||||||
newm: "从面具新建聊天",
|
newm: "从面具新建聊天",
|
||||||
|
copy: "复制当前聊天",
|
||||||
next: "下一个聊天",
|
next: "下一个聊天",
|
||||||
prev: "上一个聊天",
|
prev: "上一个聊天",
|
||||||
clear: "清除上下文",
|
clear: "清除上下文",
|
||||||
@@ -234,6 +235,10 @@ const cn = {
|
|||||||
Title: "同步类型",
|
Title: "同步类型",
|
||||||
SubTitle: "选择喜爱的同步服务器",
|
SubTitle: "选择喜爱的同步服务器",
|
||||||
},
|
},
|
||||||
|
EnableAutoSync: {
|
||||||
|
Title: "自动同步设置",
|
||||||
|
SubTitle: "在回复完成或删除消息后自动同步数据",
|
||||||
|
},
|
||||||
Proxy: {
|
Proxy: {
|
||||||
Title: "启用代理",
|
Title: "启用代理",
|
||||||
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||||
@@ -462,6 +467,17 @@ const cn = {
|
|||||||
SubTitle: "样例:",
|
SubTitle: "样例:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
XAI: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义XAI API Key",
|
||||||
|
Placeholder: "XAI API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
},
|
||||||
Stability: {
|
Stability: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "接口密钥",
|
Title: "接口密钥",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ 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",
|
||||||
@@ -236,6 +237,11 @@ 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",
|
||||||
@@ -446,6 +452,17 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Example: ",
|
SubTitle: "Example: ",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
XAI: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "XAI API Key",
|
||||||
|
SubTitle: "Use a custom XAI API Key",
|
||||||
|
Placeholder: "XAI API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
Stability: {
|
Stability: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "Stability API Key",
|
Title: "Stability API Key",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MOONSHOT_BASE_URL,
|
MOONSHOT_BASE_URL,
|
||||||
STABILITY_BASE_URL,
|
STABILITY_BASE_URL,
|
||||||
IFLYTEK_BASE_URL,
|
IFLYTEK_BASE_URL,
|
||||||
|
XAI_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";
|
||||||
@@ -44,6 +45,8 @@ const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
|
|||||||
|
|
||||||
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
||||||
|
|
||||||
|
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||||
|
|
||||||
const DEFAULT_ACCESS_STATE = {
|
const DEFAULT_ACCESS_STATE = {
|
||||||
accessCode: "",
|
accessCode: "",
|
||||||
useCustomConfig: false,
|
useCustomConfig: false,
|
||||||
@@ -101,6 +104,10 @@ const DEFAULT_ACCESS_STATE = {
|
|||||||
iflytekApiKey: "",
|
iflytekApiKey: "",
|
||||||
iflytekApiSecret: "",
|
iflytekApiSecret: "",
|
||||||
|
|
||||||
|
// xai
|
||||||
|
xaiUrl: DEFAULT_XAI_URL,
|
||||||
|
xaiApiKey: "",
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
@@ -169,6 +176,10 @@ export const useAccessStore = createPersistStore(
|
|||||||
return ensure(get(), ["iflytekApiKey"]);
|
return ensure(get(), ["iflytekApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidXAI() {
|
||||||
|
return ensure(get(), ["xaiApiKey"]);
|
||||||
|
},
|
||||||
|
|
||||||
isAuthorized() {
|
isAuthorized() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
|
|
||||||
@@ -184,6 +195,7 @@ export const useAccessStore = createPersistStore(
|
|||||||
this.isValidTencent() ||
|
this.isValidTencent() ||
|
||||||
this.isValidMoonshot() ||
|
this.isValidMoonshot() ||
|
||||||
this.isValidIflytek() ||
|
this.isValidIflytek() ||
|
||||||
|
this.isValidXAI() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||||
);
|
);
|
||||||
@@ -211,7 +223,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(() => ({ ...res }));
|
set(() => ({ lastUpdateTime: Date.now(), ...res }));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error("[Config] failed to fetch config");
|
console.error("[Config] failed to fetch config");
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { getMessageTextContent, trimTopic } from "../utils";
|
import {
|
||||||
|
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";
|
||||||
@@ -29,6 +33,7 @@ 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();
|
||||||
|
|
||||||
@@ -80,6 +85,7 @@ export interface ChatSession {
|
|||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
lastSummarizeIndex: number;
|
lastSummarizeIndex: number;
|
||||||
clearContextIndex?: number;
|
clearContextIndex?: number;
|
||||||
|
deletedMessageIds?: Record<string, number>;
|
||||||
|
|
||||||
mask: Mask;
|
mask: Mask;
|
||||||
}
|
}
|
||||||
@@ -103,6 +109,7 @@ function createEmptySession(): ChatSession {
|
|||||||
},
|
},
|
||||||
lastUpdate: Date.now(),
|
lastUpdate: Date.now(),
|
||||||
lastSummarizeIndex: 0,
|
lastSummarizeIndex: 0,
|
||||||
|
deletedMessageIds: {},
|
||||||
|
|
||||||
mask: createEmptyMask(),
|
mask: createEmptyMask(),
|
||||||
};
|
};
|
||||||
@@ -188,9 +195,19 @@ 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: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,6 +257,28 @@ 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;
|
||||||
@@ -302,7 +341,18 @@ export const useChatStore = createPersistStore(
|
|||||||
if (!deletedSession) return;
|
if (!deletedSession) return;
|
||||||
|
|
||||||
const sessions = get().sessions.slice();
|
const sessions = get().sessions.slice();
|
||||||
sessions.splice(index, 1);
|
const deletedSessionIds = { ...get().deletedSessionIds };
|
||||||
|
|
||||||
|
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(
|
||||||
@@ -319,19 +369,24 @@ 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,
|
||||||
@@ -352,6 +407,24 @@ 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();
|
||||||
@@ -359,6 +432,8 @@ 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,6 +24,7 @@ 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,
|
||||||
|
|
||||||
@@ -43,6 +44,8 @@ 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) => ({
|
||||||
@@ -89,6 +92,16 @@ 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];
|
||||||
@@ -103,9 +116,7 @@ export const useSyncStore = createPersistStore(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
const parsedRemoteState = JSON.parse(
|
const parsedRemoteState = JSON.parse(remoteState) as AppState;
|
||||||
await client.get(config.username),
|
|
||||||
) as AppState;
|
|
||||||
mergeAppState(localState, parsedRemoteState);
|
mergeAppState(localState, parsedRemoteState);
|
||||||
setLocalAppState(localState);
|
setLocalAppState(localState);
|
||||||
}
|
}
|
||||||
@@ -123,6 +134,14 @@ 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,
|
||||||
|
|||||||
13
app/utils.ts
13
app/utils.ts
@@ -274,6 +274,19 @@ 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 ||
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
|
|||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("stream error", e);
|
console.error("stream error", e);
|
||||||
throw e;
|
// throw e;
|
||||||
|
return new Response("", { status: 599 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return window.fetch(url, options);
|
return window.fetch(url, options);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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;
|
||||||
@@ -65,7 +66,10 @@ 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) => {
|
||||||
@@ -75,29 +79,98 @@ 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
|
||||||
localState.sessions.push(remoteSession);
|
if (
|
||||||
|
(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)) {
|
||||||
localSession.messages.push(m);
|
if (
|
||||||
|
!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) => {
|
||||||
@@ -153,9 +226,9 @@ export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
|
|||||||
remoteState: T,
|
remoteState: T,
|
||||||
) {
|
) {
|
||||||
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
||||||
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
|
const remoteUpdateTime = remoteState.lastUpdateTime ?? 1;
|
||||||
|
|
||||||
if (localUpdateTime < remoteUpdateTime) {
|
if (localUpdateTime >= remoteUpdateTime) {
|
||||||
merge(remoteState, localState);
|
merge(remoteState, localState);
|
||||||
return { ...remoteState };
|
return { ...remoteState };
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -119,11 +119,22 @@ pub async fn stream_fetch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error response: {:?}", err.source().expect("REASON").to_string());
|
let error: String = err.source()
|
||||||
|
.map(|e| e.to_string())
|
||||||
|
.unwrap_or_else(|| "Unknown error occurred".to_string());
|
||||||
|
println!("Error response: {:?}", error);
|
||||||
|
tauri::async_runtime::spawn( async move {
|
||||||
|
if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: error.into() }) {
|
||||||
|
println!("Failed to emit chunk payload: {:?}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) {
|
||||||
|
println!("Failed to emit end payload: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
StreamResponse {
|
StreamResponse {
|
||||||
request_id,
|
request_id,
|
||||||
status: 599,
|
status: 599,
|
||||||
status_text: err.source().expect("REASON").to_string(),
|
status_text: "Error".to_string(),
|
||||||
headers: HashMap::new(),
|
headers: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "NextChat",
|
"productName": "NextChat",
|
||||||
"version": "2.15.4"
|
"version": "2.15.6"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
|||||||
Reference in New Issue
Block a user