Merge branch 'ChatGPTNextWeb:main' into personal
This commit is contained in:
commit
1070207754
|
@ -3,9 +3,7 @@ name: VercelPreviewDeployment
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- review_requested
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
|
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "!*"
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- review_requested
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node_modules-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Run Jest tests
|
||||||
|
run: yarn test:ci
|
12
README.md
12
README.md
|
@ -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)
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||||
|
|
||||||
企业版咨询: **business@nextchat.dev**
|
企业版咨询: **business@nextchat.dev**
|
||||||
|
|
||||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
<img width="300" src="https://github.com/user-attachments/assets/3d4305ac-6e95-489e-884b-51d51db5f692">
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -100,6 +100,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
|
- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
|
||||||
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
|
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
|
||||||
- 🚀 v2.14.0 Now supports Artifacts & SD
|
- 🚀 v2.14.0 Now supports Artifacts & SD
|
||||||
- 🚀 v2.10.1 support Google Gemini Pro model.
|
- 🚀 v2.10.1 support Google Gemini Pro model.
|
||||||
|
@ -137,6 +138,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||||
|
|
||||||
## 最新动态
|
## 最新动态
|
||||||
|
|
||||||
|
- 🚀 v2.15.4 客户端支持Tauri本地直接调用大模型API,更安全
|
||||||
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
|
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
|
||||||
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
|
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
|
||||||
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
|
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
|
||||||
|
@ -332,9 +334,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
|
||||||
|
|
||||||
User `-all` to disable all default models, `+all` to enable all default models.
|
User `-all` to disable all default models, `+all` to enable all default models.
|
||||||
|
|
||||||
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
|
For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
|
||||||
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
|
> Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
|
||||||
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
|
> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
|
||||||
|
|
||||||
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
|
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
|
||||||
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
||||||
|
|
|
@ -216,9 +216,9 @@ ByteDance Api Url.
|
||||||
|
|
||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||||
|
|
||||||
在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||||
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
||||||
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
|
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
|
||||||
|
|
||||||
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||||
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
|
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
|
||||||
|
|
|
@ -207,8 +207,8 @@ ByteDance API の URL。
|
||||||
|
|
||||||
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
|
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
|
||||||
|
|
||||||
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||||
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
|
> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
|
||||||
|
|
||||||
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||||
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
|
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
|
||||||
|
|
|
@ -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")) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { requestOpenai } from "./common";
|
import { requestOpenai } from "./common";
|
||||||
|
|
||||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
|
||||||
|
|
||||||
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||||
const config = getServerSideConfig();
|
const config = getServerSideConfig();
|
||||||
|
@ -34,7 +34,7 @@ export async function handle(
|
||||||
|
|
||||||
const subpath = params.path.join("/");
|
const subpath = params.path.join("/");
|
||||||
|
|
||||||
if (!ALLOWD_PATH.has(subpath)) {
|
if (!ALLOWED_PATH.has(subpath)) {
|
||||||
console.log("[OpenAI Route] forbidden path ", subpath);
|
console.log("[OpenAI Route] forbidden path ", subpath);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
|
@ -178,6 +179,7 @@ export class QwenApi implements LLMApi {
|
||||||
controller.signal.onabort = finish;
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
|
fetch: fetch as any,
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -8,11 +8,12 @@ import {
|
||||||
ChatMessageTool,
|
ChatMessageTool,
|
||||||
} from "@/app/store";
|
} from "@/app/store";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
import { ANTHROPIC_BASE_URL } from "@/app/constant";
|
||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
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";
|
||||||
|
@ -388,9 +389,7 @@ export class ClaudeApi implements LLMApi {
|
||||||
if (baseUrl.trim().length === 0) {
|
if (baseUrl.trim().length === 0) {
|
||||||
const isApp = !!getClientConfig()?.isApp;
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
|
||||||
baseUrl = isApp
|
baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
|
||||||
: ApiPath.Anthropic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
|
@ -197,6 +198,7 @@ export class ErnieApi implements LLMApi {
|
||||||
controller.signal.onabort = finish;
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
|
fetch: fetch as any,
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
|
@ -165,6 +166,7 @@ export class DoubaoApi implements LLMApi {
|
||||||
controller.signal.onabort = finish;
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
|
fetch: fetch as any,
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -7,21 +7,26 @@ import {
|
||||||
LLMUsage,
|
LLMUsage,
|
||||||
SpeechOptions,
|
SpeechOptions,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
|
||||||
import { getClientConfig } from "@/app/config/client";
|
|
||||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
|
||||||
import Locale from "../../locales";
|
|
||||||
import {
|
import {
|
||||||
EventStreamContentType,
|
useAccessStore,
|
||||||
fetchEventSource,
|
useAppConfig,
|
||||||
} from "@fortaine/fetch-event-source";
|
useChatStore,
|
||||||
import { prettyObject } from "@/app/utils/format";
|
usePluginStore,
|
||||||
|
ChatMessageTool,
|
||||||
|
} from "@/app/store";
|
||||||
|
import { stream } from "@/app/utils/chat";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { GEMINI_BASE_URL } from "@/app/constant";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
import { preProcessImageContent } from "@/app/utils/chat";
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { RequestPayload } from "./openai";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export class GeminiProApi implements LLMApi {
|
export class GeminiProApi implements LLMApi {
|
||||||
path(path: string): string {
|
path(path: string): string {
|
||||||
|
@ -34,7 +39,7 @@ export class GeminiProApi implements LLMApi {
|
||||||
|
|
||||||
const isApp = !!getClientConfig()?.isApp;
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
if (baseUrl.length === 0) {
|
if (baseUrl.length === 0) {
|
||||||
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
|
baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||||
}
|
}
|
||||||
if (baseUrl.endsWith("/")) {
|
if (baseUrl.endsWith("/")) {
|
||||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
@ -177,114 +182,84 @@ export class GeminiProApi implements LLMApi {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
let responseText = "";
|
const [tools, funcs] = usePluginStore
|
||||||
let remainText = "";
|
.getState()
|
||||||
let finished = false;
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
|
);
|
||||||
|
return stream(
|
||||||
|
chatPath,
|
||||||
|
requestPayload,
|
||||||
|
getHeaders(),
|
||||||
|
// @ts-ignore
|
||||||
|
tools.length > 0
|
||||||
|
? // @ts-ignore
|
||||||
|
[{ functionDeclarations: tools.map((tool) => tool.function) }]
|
||||||
|
: [],
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
const chunkJson = JSON.parse(text);
|
||||||
|
|
||||||
const finish = () => {
|
const functionCall = chunkJson?.candidates
|
||||||
if (!finished) {
|
?.at(0)
|
||||||
finished = true;
|
?.content.parts.at(0)?.functionCall;
|
||||||
options.onFinish(responseText + remainText);
|
if (functionCall) {
|
||||||
}
|
const { name, args } = functionCall;
|
||||||
};
|
runTools.push({
|
||||||
|
id: nanoid(),
|
||||||
// animate response to make it looks smooth
|
type: "function",
|
||||||
function animateResponseText() {
|
function: {
|
||||||
if (finished || controller.signal.aborted) {
|
name,
|
||||||
responseText += remainText;
|
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
|
||||||
finish();
|
},
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
|
||||||
if (remainText.length > 0) {
|
},
|
||||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
const fetchText = remainText.slice(0, fetchCount);
|
(
|
||||||
responseText += fetchText;
|
requestPayload: RequestPayload,
|
||||||
remainText = remainText.slice(fetchCount);
|
toolCallMessage: any,
|
||||||
options.onUpdate?.(responseText, fetchText);
|
toolCallResult: any[],
|
||||||
}
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
requestAnimationFrame(animateResponseText);
|
requestPayload?.contents?.splice(
|
||||||
}
|
// @ts-ignore
|
||||||
|
requestPayload?.contents?.length,
|
||||||
// start animaion
|
0,
|
||||||
animateResponseText();
|
{
|
||||||
|
role: "model",
|
||||||
controller.signal.onabort = finish;
|
parts: toolCallMessage.tool_calls.map(
|
||||||
|
(tool: ChatMessageTool) => ({
|
||||||
fetchEventSource(chatPath, {
|
functionCall: {
|
||||||
...chatPayload,
|
name: tool?.function?.name,
|
||||||
async onopen(res) {
|
args: JSON.parse(tool?.function?.arguments as string),
|
||||||
clearTimeout(requestTimeoutId);
|
},
|
||||||
const contentType = res.headers.get("content-type");
|
}),
|
||||||
console.log(
|
),
|
||||||
"[Gemini] request response content type: ",
|
},
|
||||||
contentType,
|
// @ts-ignore
|
||||||
|
...toolCallResult.map((result) => ({
|
||||||
|
role: "function",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
name: result.name,
|
||||||
|
response: {
|
||||||
|
name: result.name,
|
||||||
|
content: result.content, // TODO just text content...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentType?.startsWith("text/plain")) {
|
|
||||||
responseText = await res.clone().text();
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
!res.headers
|
|
||||||
.get("content-type")
|
|
||||||
?.startsWith(EventStreamContentType) ||
|
|
||||||
res.status !== 200
|
|
||||||
) {
|
|
||||||
const responseTexts = [responseText];
|
|
||||||
let extraInfo = await res.clone().text();
|
|
||||||
try {
|
|
||||||
const resJson = await res.clone().json();
|
|
||||||
extraInfo = prettyObject(resJson);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
responseTexts.push(Locale.Error.Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extraInfo) {
|
|
||||||
responseTexts.push(extraInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
responseText = responseTexts.join("\n\n");
|
|
||||||
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onmessage(msg) {
|
options,
|
||||||
if (msg.data === "[DONE]" || finished) {
|
);
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
const text = msg.data;
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(text);
|
|
||||||
const delta = apiClient.extractMessage(json);
|
|
||||||
|
|
||||||
if (delta) {
|
|
||||||
remainText += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockReason = json?.promptFeedback?.blockReason;
|
|
||||||
if (blockReason) {
|
|
||||||
// being blocked
|
|
||||||
console.log(`[Google] [Safety Ratings] result:`, blockReason);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Request] parse error", text, msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
finish();
|
|
||||||
},
|
|
||||||
onerror(e) {
|
|
||||||
options.onError?.(e);
|
|
||||||
throw e;
|
|
||||||
},
|
|
||||||
openWhenHidden: true,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
ApiPath,
|
ApiPath,
|
||||||
DEFAULT_API_HOST,
|
IFLYTEK_BASE_URL,
|
||||||
Iflytek,
|
Iflytek,
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
} from "@/app/constant";
|
} from "@/app/constant";
|
||||||
|
@ -22,6 +22,7 @@ import {
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
import { RequestPayload } from "./openai";
|
import { RequestPayload } from "./openai";
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ export class SparkApi implements LLMApi {
|
||||||
if (baseUrl.length === 0) {
|
if (baseUrl.length === 0) {
|
||||||
const isApp = !!getClientConfig()?.isApp;
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
const apiPath = ApiPath.Iflytek;
|
const apiPath = ApiPath.Iflytek;
|
||||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseUrl.endsWith("/")) {
|
if (baseUrl.endsWith("/")) {
|
||||||
|
@ -149,6 +150,7 @@ export class SparkApi implements LLMApi {
|
||||||
controller.signal.onabort = finish;
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
|
fetch: fetch as any,
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// azure and openai, using same models. so using same LLMApi.
|
// azure and openai, using same models. so using same LLMApi.
|
||||||
import {
|
import {
|
||||||
ApiPath,
|
ApiPath,
|
||||||
DEFAULT_API_HOST,
|
MOONSHOT_BASE_URL,
|
||||||
Moonshot,
|
Moonshot,
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
} from "@/app/constant";
|
} from "@/app/constant";
|
||||||
|
@ -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;
|
||||||
|
@ -40,7 +41,7 @@ export class MoonshotApi implements LLMApi {
|
||||||
if (baseUrl.length === 0) {
|
if (baseUrl.length === 0) {
|
||||||
const isApp = !!getClientConfig()?.isApp;
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
const apiPath = ApiPath.Moonshot;
|
const apiPath = ApiPath.Moonshot;
|
||||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseUrl.endsWith("/")) {
|
if (baseUrl.endsWith("/")) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// azure and openai, using same models. so using same LLMApi.
|
// azure and openai, using same models. so using same LLMApi.
|
||||||
import {
|
import {
|
||||||
ApiPath,
|
ApiPath,
|
||||||
DEFAULT_API_HOST,
|
OPENAI_BASE_URL,
|
||||||
DEFAULT_MODELS,
|
DEFAULT_MODELS,
|
||||||
OpenaiPath,
|
OpenaiPath,
|
||||||
Azure,
|
Azure,
|
||||||
|
@ -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;
|
||||||
|
@ -98,7 +99,7 @@ export class ChatGPTApi implements LLMApi {
|
||||||
if (baseUrl.length === 0) {
|
if (baseUrl.length === 0) {
|
||||||
const isApp = !!getClientConfig()?.isApp;
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
|
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
|
||||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseUrl.endsWith("/")) {
|
if (baseUrl.endsWith("/")) {
|
||||||
|
@ -352,7 +353,7 @@ export class ChatGPTApi implements LLMApi {
|
||||||
// make a fetch request
|
// make a fetch request
|
||||||
const requestTimeoutId = setTimeout(
|
const requestTimeoutId = setTimeout(
|
||||||
() => controller.abort(),
|
() => controller.abort(),
|
||||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -22,6 +22,7 @@ import mapKeys from "lodash-es/mapKeys";
|
||||||
import mapValues from "lodash-es/mapValues";
|
import mapValues from "lodash-es/mapValues";
|
||||||
import isArray from "lodash-es/isArray";
|
import isArray from "lodash-es/isArray";
|
||||||
import isObject from "lodash-es/isObject";
|
import isObject from "lodash-es/isObject";
|
||||||
|
import { fetch } from "@/app/utils/stream";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
|
@ -70,9 +71,7 @@ export class HunyuanApi implements LLMApi {
|
||||||
|
|
||||||
if (baseUrl.length === 0) {
|
if (baseUrl.length === 0) {
|
||||||
const isApp = !!getClientConfig()?.isApp;
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
baseUrl = isApp
|
baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
|
||||||
: ApiPath.Tencent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (baseUrl.endsWith("/")) {
|
if (baseUrl.endsWith("/")) {
|
||||||
|
@ -179,6 +178,7 @@ export class HunyuanApi implements LLMApi {
|
||||||
controller.signal.onabort = finish;
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
|
fetch: fetch as any,
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -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 [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import Logo from "../icons/logo.svg";
|
||||||
import { useMobileScreen } from "@/app/utils";
|
import { useMobileScreen } from "@/app/utils";
|
||||||
import BotIcon from "../icons/bot.svg";
|
import BotIcon from "../icons/bot.svg";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
|
import { PasswordInput } from "./ui-lib";
|
||||||
import LeftIcon from "@/app/icons/left.svg";
|
import LeftIcon from "@/app/icons/left.svg";
|
||||||
import { safeLocalStorage } from "@/app/utils";
|
import { safeLocalStorage } from "@/app/utils";
|
||||||
import {
|
import {
|
||||||
|
@ -60,36 +61,43 @@ export function AuthPage() {
|
||||||
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
||||||
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
||||||
|
|
||||||
<input
|
<PasswordInput
|
||||||
className={styles["auth-input"]}
|
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||||
type="password"
|
aria={Locale.Settings.ShowPassword}
|
||||||
placeholder={Locale.Auth.Input}
|
aria-label={Locale.Auth.Input}
|
||||||
value={accessStore.accessCode}
|
value={accessStore.accessCode}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Auth.Input}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.accessCode = e.currentTarget.value),
|
(access) => (access.accessCode = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!accessStore.hideUserApiKey ? (
|
{!accessStore.hideUserApiKey ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
||||||
<input
|
<PasswordInput
|
||||||
className={styles["auth-input"]}
|
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||||
type="password"
|
aria={Locale.Settings.ShowPassword}
|
||||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||||
value={accessStore.openaiApiKey}
|
value={accessStore.openaiApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<PasswordInput
|
||||||
className={styles["auth-input-second"]}
|
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||||
type="password"
|
aria={Locale.Settings.ShowPassword}
|
||||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||||
value={accessStore.googleApiKey}
|
value={accessStore.googleApiKey}
|
||||||
|
type="text"
|
||||||
|
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
accessStore.update(
|
accessStore.update(
|
||||||
(access) => (access.googleApiKey = e.currentTarget.value),
|
(access) => (access.googleApiKey = e.currentTarget.value),
|
||||||
|
|
|
@ -115,11 +115,14 @@ import { getClientConfig } from "../config/client";
|
||||||
import { useAllModels } from "../utils/hooks";
|
import { useAllModels } from "../utils/hooks";
|
||||||
import { MultimodalContent } from "../client/api";
|
import { MultimodalContent } from "../client/api";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
|
||||||
import { ClientApi } from "../client/api";
|
import { ClientApi } from "../client/api";
|
||||||
import { createTTSPlayer } from "../utils/audio";
|
import { createTTSPlayer } from "../utils/audio";
|
||||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||||
|
|
||||||
|
import { isEmpty } from "lodash-es";
|
||||||
|
|
||||||
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
const ttsPlayer = createTTSPlayer();
|
const ttsPlayer = createTTSPlayer();
|
||||||
|
|
||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
|
@ -1015,7 +1018,7 @@ function _Chat() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const doSubmit = (userInput: string) => {
|
const doSubmit = (userInput: string) => {
|
||||||
if (userInput.trim() === "") return;
|
if (userInput.trim() === "" && isEmpty(attachImages)) return;
|
||||||
const matchCommand = chatCommands.match(userInput);
|
const matchCommand = chatCommands.match(userInput);
|
||||||
if (matchCommand.matched) {
|
if (matchCommand.matched) {
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
|
@ -1815,6 +1818,7 @@ function _Chat() {
|
||||||
{message?.tools?.map((tool) => (
|
{message?.tools?.map((tool) => (
|
||||||
<div
|
<div
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
|
title={tool?.errorMsg}
|
||||||
className={styles["chat-message-tool"]}
|
className={styles["chat-message-tool"]}
|
||||||
>
|
>
|
||||||
{tool.isError === false ? (
|
{tool.isError === false ? (
|
||||||
|
|
|
@ -140,6 +140,9 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
&-narrow {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
|
|
|
@ -169,6 +169,12 @@ export function PreCode(props: { children: any }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomCode(props: { children: any; className?: string }) {
|
function CustomCode(props: { children: any; className?: string }) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const config = useAppConfig();
|
||||||
|
const enableCodeFold =
|
||||||
|
session.mask?.enableCodeFold !== false && config.enableCodeFold;
|
||||||
|
|
||||||
const ref = useRef<HTMLPreElement>(null);
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const [showToggle, setShowToggle] = useState(false);
|
const [showToggle, setShowToggle] = useState(false);
|
||||||
|
@ -184,46 +190,34 @@ function CustomCode(props: { children: any; className?: string }) {
|
||||||
const toggleCollapsed = () => {
|
const toggleCollapsed = () => {
|
||||||
setCollapsed((collapsed) => !collapsed);
|
setCollapsed((collapsed) => !collapsed);
|
||||||
};
|
};
|
||||||
|
const renderShowMoreButton = () => {
|
||||||
|
if (showToggle && enableCodeFold && collapsed) {
|
||||||
|
return (
|
||||||
|
<div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}>
|
||||||
|
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<code
|
<code
|
||||||
className={props?.className}
|
className={props?.className}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: collapsed ? "400px" : "none",
|
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
|
||||||
overflowY: "hidden",
|
overflowY: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</code>
|
</code>
|
||||||
{showToggle && collapsed && (
|
|
||||||
<div
|
{renderShowMoreButton()}
|
||||||
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
|
||||||
>
|
|
||||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeDollarNumber(text: string) {
|
|
||||||
let escapedText = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < text.length; i += 1) {
|
|
||||||
let char = text[i];
|
|
||||||
const nextChar = text[i + 1] || " ";
|
|
||||||
|
|
||||||
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
|
||||||
char = "\\$";
|
|
||||||
}
|
|
||||||
|
|
||||||
escapedText += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
return escapedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeBrackets(text: string) {
|
function escapeBrackets(text: string) {
|
||||||
const pattern =
|
const pattern =
|
||||||
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
||||||
|
@ -261,7 +255,7 @@ function tryWrapHtmlCode(text: string) {
|
||||||
|
|
||||||
function _MarkDownContent(props: { content: string }) {
|
function _MarkDownContent(props: { content: string }) {
|
||||||
const escapedContent = useMemo(() => {
|
const escapedContent = useMemo(() => {
|
||||||
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
|
return tryWrapHtmlCode(escapeBrackets(props.content));
|
||||||
}, [props.content]);
|
}, [props.content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -183,6 +183,23 @@ export function MaskConfig(props: {
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
{globalConfig.enableCodeFold && (
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
type="checkbox"
|
||||||
|
checked={props.mask.enableCodeFold !== false}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.updateMask((mask) => {
|
||||||
|
mask.enableCodeFold = e.currentTarget.checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
|
||||||
{!props.shouldSyncFromGlobal ? (
|
{!props.shouldSyncFromGlobal ? (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|
|
@ -49,7 +49,7 @@ import Locale, {
|
||||||
changeLang,
|
changeLang,
|
||||||
getLang,
|
getLang,
|
||||||
} from "../locales";
|
} from "../locales";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
Anthropic,
|
Anthropic,
|
||||||
|
@ -59,6 +59,7 @@ import {
|
||||||
ByteDance,
|
ByteDance,
|
||||||
Alibaba,
|
Alibaba,
|
||||||
Moonshot,
|
Moonshot,
|
||||||
|
XAI,
|
||||||
Google,
|
Google,
|
||||||
GoogleSafetySettingsThreshold,
|
GoogleSafetySettingsThreshold,
|
||||||
OPENAI_BASE_URL,
|
OPENAI_BASE_URL,
|
||||||
|
@ -585,7 +586,7 @@ export function Settings() {
|
||||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||||
const currentVersion = updateStore.formatVersion(updateStore.version);
|
const currentVersion = updateStore.formatVersion(updateStore.version);
|
||||||
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
|
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
|
||||||
const hasNewVersion = currentVersion !== remoteId;
|
const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
|
||||||
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
|
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
|
||||||
|
|
||||||
function checkUpdate(force = false) {
|
function checkUpdate(force = false) {
|
||||||
|
@ -1194,6 +1195,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 && (
|
||||||
<>
|
<>
|
||||||
|
@ -1357,9 +1397,17 @@ export function Settings() {
|
||||||
{checkingUpdate ? (
|
{checkingUpdate ? (
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
) : hasNewVersion ? (
|
) : hasNewVersion ? (
|
||||||
<Link href={updateUrl} target="_blank" className="link">
|
clientConfig?.isApp ? (
|
||||||
{Locale.Settings.Update.GoToUpdate}
|
<IconButton
|
||||||
</Link>
|
icon={<ResetIcon></ResetIcon>}
|
||||||
|
text={Locale.Settings.Update.GoToUpdate}
|
||||||
|
onClick={() => clientUpdate()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Link href={updateUrl} target="_blank" className="link">
|
||||||
|
{Locale.Settings.Update.GoToUpdate}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ResetIcon></ResetIcon>}
|
icon={<ResetIcon></ResetIcon>}
|
||||||
|
@ -1509,6 +1557,22 @@ export function Settings() {
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.enableCodeFold}
|
||||||
|
data-testid="enable-code-fold-checkbox"
|
||||||
|
onChange={(e) =>
|
||||||
|
updateConfig(
|
||||||
|
(config) => (config.enableCodeFold = e.currentTarget.checked),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<SyncItems />
|
<SyncItems />
|
||||||
|
@ -1628,6 +1692,7 @@ export function Settings() {
|
||||||
{moonshotConfigComponent}
|
{moonshotConfigComponent}
|
||||||
{stabilityConfigComponent}
|
{stabilityConfigComponent}
|
||||||
{lflytekConfigComponent}
|
{lflytekConfigComponent}
|
||||||
|
{XAIConfigComponent}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -165,11 +165,17 @@ export function SideBarHeader(props: {
|
||||||
subTitle?: string | React.ReactNode;
|
subTitle?: string | React.ReactNode;
|
||||||
logo?: React.ReactNode;
|
logo?: React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
shouldNarrow?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { title, subTitle, logo, children } = props;
|
const { title, subTitle, logo, children, shouldNarrow } = props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
<div
|
||||||
|
className={`${styles["sidebar-header"]} ${
|
||||||
|
shouldNarrow ? styles["sidebar-header-narrow"] : ""
|
||||||
|
}`}
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
<div className={styles["sidebar-title-container"]}>
|
<div className={styles["sidebar-title-container"]}>
|
||||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||||
{title}
|
{title}
|
||||||
|
@ -227,6 +233,7 @@ export function SideBar(props: { className?: string }) {
|
||||||
title="NextChat"
|
title="NextChat"
|
||||||
subTitle="Build your own AI assistant."
|
subTitle="Build your own AI assistant."
|
||||||
logo={<ChatGptIcon />}
|
logo={<ChatGptIcon />}
|
||||||
|
shouldNarrow={shouldNarrow}
|
||||||
>
|
>
|
||||||
<div className={styles["sidebar-header-bar"]}>
|
<div className={styles["sidebar-header-bar"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -11,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
|
|
||||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||||
|
|
||||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
|
||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||||
|
|
||||||
|
@ -29,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`;
|
||||||
|
|
||||||
|
@ -60,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 {
|
||||||
|
@ -112,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
|
||||||
|
@ -134,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 = {
|
||||||
|
@ -216,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}}.
|
||||||
|
@ -365,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) => ({
|
||||||
|
@ -477,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;
|
||||||
|
|
|
@ -26,6 +26,13 @@ declare interface Window {
|
||||||
isPermissionGranted(): Promise<boolean>;
|
isPermissionGranted(): Promise<boolean>;
|
||||||
sendNotification(options: string | Options): void;
|
sendNotification(options: string | Options): void;
|
||||||
};
|
};
|
||||||
|
updater: {
|
||||||
|
checkUpdate(): Promise<UpdateResult>;
|
||||||
|
installUpdate(): Promise<void>;
|
||||||
|
onUpdaterEvent(
|
||||||
|
handler: (status: UpdateStatusResult) => void,
|
||||||
|
): Promise<UnlistenFn>;
|
||||||
|
};
|
||||||
http: {
|
http: {
|
||||||
fetch<T>(
|
fetch<T>(
|
||||||
url: string,
|
url: string,
|
||||||
|
|
|
@ -205,6 +205,8 @@ const cn = {
|
||||||
IsChecking: "正在检查更新...",
|
IsChecking: "正在检查更新...",
|
||||||
FoundUpdate: (x: string) => `发现新版本:${x}`,
|
FoundUpdate: (x: string) => `发现新版本:${x}`,
|
||||||
GoToUpdate: "前往更新",
|
GoToUpdate: "前往更新",
|
||||||
|
Success: "更新成功!",
|
||||||
|
Failed: "更新失败",
|
||||||
},
|
},
|
||||||
SendKey: "发送键",
|
SendKey: "发送键",
|
||||||
Theme: "主题",
|
Theme: "主题",
|
||||||
|
@ -460,6 +462,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: "接口密钥",
|
||||||
|
@ -495,8 +508,8 @@ const cn = {
|
||||||
|
|
||||||
Model: "模型 (model)",
|
Model: "模型 (model)",
|
||||||
CompressModel: {
|
CompressModel: {
|
||||||
Title: "压缩模型",
|
Title: "对话摘要模型",
|
||||||
SubTitle: "用于压缩历史记录的模型",
|
SubTitle: "用于压缩历史记录、生成对话标题的模型",
|
||||||
},
|
},
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "随机性 (temperature)",
|
Title: "随机性 (temperature)",
|
||||||
|
@ -665,6 +678,10 @@ const cn = {
|
||||||
Title: "启用Artifacts",
|
Title: "启用Artifacts",
|
||||||
SubTitle: "启用之后可以直接渲染HTML页面",
|
SubTitle: "启用之后可以直接渲染HTML页面",
|
||||||
},
|
},
|
||||||
|
CodeFold: {
|
||||||
|
Title: "启用代码折叠",
|
||||||
|
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
|
||||||
|
},
|
||||||
Share: {
|
Share: {
|
||||||
Title: "分享此面具",
|
Title: "分享此面具",
|
||||||
SubTitle: "生成此面具的直达链接",
|
SubTitle: "生成此面具的直达链接",
|
||||||
|
|
|
@ -207,6 +207,8 @@ const en: LocaleType = {
|
||||||
IsChecking: "Checking update...",
|
IsChecking: "Checking update...",
|
||||||
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
||||||
GoToUpdate: "Update",
|
GoToUpdate: "Update",
|
||||||
|
Success: "Update Successful.",
|
||||||
|
Failed: "Update Failed.",
|
||||||
},
|
},
|
||||||
SendKey: "Send Key",
|
SendKey: "Send Key",
|
||||||
Theme: "Theme",
|
Theme: "Theme",
|
||||||
|
@ -444,6 +446,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",
|
||||||
|
@ -500,8 +513,8 @@ const en: LocaleType = {
|
||||||
|
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
CompressModel: {
|
CompressModel: {
|
||||||
Title: "Compression Model",
|
Title: "Summary Model",
|
||||||
SubTitle: "Model used to compress history",
|
SubTitle: "Model used to compress history and generate title",
|
||||||
},
|
},
|
||||||
Temperature: {
|
Temperature: {
|
||||||
Title: "Temperature",
|
Title: "Temperature",
|
||||||
|
@ -675,6 +688,11 @@ const en: LocaleType = {
|
||||||
Title: "Enable Artifacts",
|
Title: "Enable Artifacts",
|
||||||
SubTitle: "Can render HTML page when enable artifacts.",
|
SubTitle: "Can render HTML page when enable artifacts.",
|
||||||
},
|
},
|
||||||
|
CodeFold: {
|
||||||
|
Title: "Enable CodeFold",
|
||||||
|
SubTitle:
|
||||||
|
"Automatically collapse/expand overly long code blocks when CodeFold is enabled",
|
||||||
|
},
|
||||||
Share: {
|
Share: {
|
||||||
Title: "Share This Mask",
|
Title: "Share This Mask",
|
||||||
SubTitle: "Generate a link to this mask",
|
SubTitle: "Generate a link to this mask",
|
||||||
|
|
|
@ -8,12 +8,12 @@ const tw = {
|
||||||
Error: {
|
Error: {
|
||||||
Unauthorized: isApp
|
Unauthorized: isApp
|
||||||
? `😆 對話遇到了一些問題,不用慌:
|
? `😆 對話遇到了一些問題,不用慌:
|
||||||
\\ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
\\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||||
\\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️`
|
\\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️`
|
||||||
: `😆 對話遇到了一些問題,不用慌:
|
: `😆 對話遇到了一些問題,不用慌:
|
||||||
\ 1️⃣ 想要零配置開箱即用,[點擊這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||||
\ 2️⃣ 如果你正在使用私有部署版本,點擊[這裡](/#/auth)輸入訪問秘鑰 🔑
|
\ 2️⃣ 如果你正在使用私有部署版本,點選[這裡](/#/auth)輸入存取金鑰 🔑
|
||||||
\ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點擊[這裡](/#/settings)修改設定 ⚙️
|
\ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -25,9 +25,9 @@ const tw = {
|
||||||
Confirm: "確認",
|
Confirm: "確認",
|
||||||
Later: "稍候再說",
|
Later: "稍候再說",
|
||||||
Return: "返回",
|
Return: "返回",
|
||||||
SaasTips: "配置太麻煩,想要立即使用",
|
SaasTips: "設定太麻煩,想要立即使用",
|
||||||
TopTips:
|
TopTips:
|
||||||
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
|
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新的大型語言模型",
|
||||||
},
|
},
|
||||||
ChatItem: {
|
ChatItem: {
|
||||||
ChatItemCount: (count: number) => `${count} 則對話`,
|
ChatItemCount: (count: number) => `${count} 則對話`,
|
||||||
|
@ -53,8 +53,8 @@ const tw = {
|
||||||
PinToastAction: "檢視",
|
PinToastAction: "檢視",
|
||||||
Delete: "刪除",
|
Delete: "刪除",
|
||||||
Edit: "編輯",
|
Edit: "編輯",
|
||||||
RefreshTitle: "刷新標題",
|
RefreshTitle: "重新整理標題",
|
||||||
RefreshToast: "已發送刷新標題請求",
|
RefreshToast: "已傳送重新整理標題請求",
|
||||||
},
|
},
|
||||||
Commands: {
|
Commands: {
|
||||||
new: "新建聊天",
|
new: "新建聊天",
|
||||||
|
@ -95,10 +95,10 @@ const tw = {
|
||||||
IsContext: "預設提示詞",
|
IsContext: "預設提示詞",
|
||||||
ShortcutKey: {
|
ShortcutKey: {
|
||||||
Title: "鍵盤快捷方式",
|
Title: "鍵盤快捷方式",
|
||||||
newChat: "打開新聊天",
|
newChat: "開啟新聊天",
|
||||||
focusInput: "聚焦輸入框",
|
focusInput: "聚焦輸入框",
|
||||||
copyLastMessage: "複製最後一個回覆",
|
copyLastMessage: "複製最後一個回覆",
|
||||||
copyLastCode: "複製最後一個代碼塊",
|
copyLastCode: "複製最後一個程式碼區塊",
|
||||||
showShortcutKey: "顯示快捷方式",
|
showShortcutKey: "顯示快捷方式",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -174,9 +174,9 @@ const tw = {
|
||||||
SubTitle: "聊天內容的字型大小",
|
SubTitle: "聊天內容的字型大小",
|
||||||
},
|
},
|
||||||
FontFamily: {
|
FontFamily: {
|
||||||
Title: "聊天字體",
|
Title: "聊天字型",
|
||||||
SubTitle: "聊天內容的字體,若置空則應用全局默認字體",
|
SubTitle: "聊天內容的字型,若留空則套用全域預設字型",
|
||||||
Placeholder: "字體名稱",
|
Placeholder: "字型名稱",
|
||||||
},
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "匯入系統提示",
|
Title: "匯入系統提示",
|
||||||
|
@ -301,8 +301,8 @@ const tw = {
|
||||||
Title: "使用 NextChat AI",
|
Title: "使用 NextChat AI",
|
||||||
Label: "(性價比最高的方案)",
|
Label: "(性價比最高的方案)",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
"由 NextChat 官方維護,零配置開箱即用,支持 OpenAI o1、GPT-4o、Claude-3.5 等最新大模型",
|
"由 NextChat 官方維護,無須設定開箱即用,支援 OpenAI o1、GPT-4o、Claude-3.5 等最新的大型語言模型",
|
||||||
ChatNow: "立刻對話",
|
ChatNow: "立刻開始對話",
|
||||||
},
|
},
|
||||||
|
|
||||||
AccessCode: {
|
AccessCode: {
|
||||||
|
@ -485,18 +485,18 @@ const tw = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SearchChat: {
|
SearchChat: {
|
||||||
Name: "搜索",
|
Name: "搜尋",
|
||||||
Page: {
|
Page: {
|
||||||
Title: "搜索聊天記錄",
|
Title: "搜尋聊天記錄",
|
||||||
Search: "輸入搜索關鍵詞",
|
Search: "輸入搜尋關鍵詞",
|
||||||
NoResult: "沒有找到結果",
|
NoResult: "沒有找到結果",
|
||||||
NoData: "沒有數據",
|
NoData: "沒有資料",
|
||||||
Loading: "加載中",
|
Loading: "載入中",
|
||||||
|
|
||||||
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
||||||
},
|
},
|
||||||
Item: {
|
Item: {
|
||||||
View: "查看",
|
View: "檢視",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NewChat: {
|
NewChat: {
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import {
|
import {
|
||||||
ApiPath,
|
|
||||||
DEFAULT_API_HOST,
|
|
||||||
GoogleSafetySettingsThreshold,
|
GoogleSafetySettingsThreshold,
|
||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
StoreKey,
|
StoreKey,
|
||||||
|
ApiPath,
|
||||||
|
OPENAI_BASE_URL,
|
||||||
|
ANTHROPIC_BASE_URL,
|
||||||
|
GEMINI_BASE_URL,
|
||||||
|
BAIDU_BASE_URL,
|
||||||
|
BYTEDANCE_BASE_URL,
|
||||||
|
ALIBABA_BASE_URL,
|
||||||
|
TENCENT_BASE_URL,
|
||||||
|
MOONSHOT_BASE_URL,
|
||||||
|
STABILITY_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";
|
||||||
|
@ -15,45 +25,27 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||||
|
|
||||||
const isApp = getClientConfig()?.buildMode === "export";
|
const isApp = getClientConfig()?.buildMode === "export";
|
||||||
|
|
||||||
const DEFAULT_OPENAI_URL = isApp
|
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/openai"
|
|
||||||
: ApiPath.OpenAI;
|
|
||||||
|
|
||||||
const DEFAULT_GOOGLE_URL = isApp
|
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/google"
|
|
||||||
: ApiPath.Google;
|
|
||||||
|
|
||||||
const DEFAULT_ANTHROPIC_URL = isApp
|
const DEFAULT_ANTHROPIC_URL = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
|
||||||
: ApiPath.Anthropic;
|
|
||||||
|
|
||||||
const DEFAULT_BAIDU_URL = isApp
|
const DEFAULT_BAIDU_URL = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/baidu"
|
|
||||||
: ApiPath.Baidu;
|
|
||||||
|
|
||||||
const DEFAULT_BYTEDANCE_URL = isApp
|
const DEFAULT_BYTEDANCE_URL = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/bytedance"
|
|
||||||
: ApiPath.ByteDance;
|
|
||||||
|
|
||||||
const DEFAULT_ALIBABA_URL = isApp
|
const DEFAULT_ALIBABA_URL = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
|
||||||
: ApiPath.Alibaba;
|
|
||||||
|
|
||||||
const DEFAULT_TENCENT_URL = isApp
|
const DEFAULT_TENCENT_URL = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
|
||||||
: ApiPath.Tencent;
|
|
||||||
|
|
||||||
const DEFAULT_MOONSHOT_URL = isApp
|
const DEFAULT_MOONSHOT_URL = isApp ? MOONSHOT_BASE_URL : ApiPath.Moonshot;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/moonshot"
|
|
||||||
: ApiPath.Moonshot;
|
|
||||||
|
|
||||||
const DEFAULT_STABILITY_URL = isApp
|
const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/stability"
|
|
||||||
: ApiPath.Stability;
|
|
||||||
|
|
||||||
const DEFAULT_IFLYTEK_URL = isApp
|
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
||||||
? DEFAULT_API_HOST + "/api/proxy/iflytek"
|
|
||||||
: ApiPath.Iflytek;
|
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||||
|
|
||||||
const DEFAULT_ACCESS_STATE = {
|
const DEFAULT_ACCESS_STATE = {
|
||||||
accessCode: "",
|
accessCode: "",
|
||||||
|
@ -112,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,
|
||||||
|
@ -180,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();
|
||||||
|
|
||||||
|
@ -195,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"]))
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,6 +16,9 @@ import {
|
||||||
DEFAULT_SYSTEM_TEMPLATE,
|
DEFAULT_SYSTEM_TEMPLATE,
|
||||||
KnowledgeCutOffDate,
|
KnowledgeCutOffDate,
|
||||||
StoreKey,
|
StoreKey,
|
||||||
|
SUMMARIZE_MODEL,
|
||||||
|
GEMINI_SUMMARIZE_MODEL,
|
||||||
|
ServiceProvider,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import Locale, { getLang } from "../locales";
|
import Locale, { getLang } from "../locales";
|
||||||
import { isDalle3, safeLocalStorage } from "../utils";
|
import { isDalle3, safeLocalStorage } from "../utils";
|
||||||
|
@ -23,6 +26,8 @@ import { prettyObject } from "../utils/format";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { estimateTokenLength } from "../utils/token";
|
import { estimateTokenLength } from "../utils/token";
|
||||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||||
|
import { useAccessStore } from "./access";
|
||||||
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { createEmptyMask, Mask } from "./mask";
|
import { createEmptyMask, Mask } from "./mask";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
|
@ -37,6 +42,7 @@ export type ChatMessageTool = {
|
||||||
};
|
};
|
||||||
content?: string;
|
content?: string;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
|
errorMsg?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatMessage = RequestMessage & {
|
export type ChatMessage = RequestMessage & {
|
||||||
|
@ -102,6 +108,35 @@ function createEmptySession(): ChatSession {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSummarizeModel(
|
||||||
|
currentModel: string,
|
||||||
|
providerName: string,
|
||||||
|
): string[] {
|
||||||
|
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||||
|
if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
|
||||||
|
const configStore = useAppConfig.getState();
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
const allModel = collectModelsWithDefaultModel(
|
||||||
|
configStore.models,
|
||||||
|
[configStore.customModels, accessStore.customModels].join(","),
|
||||||
|
accessStore.defaultModel,
|
||||||
|
);
|
||||||
|
const summarizeModel = allModel.find(
|
||||||
|
(m) => m.name === SUMMARIZE_MODEL && m.available,
|
||||||
|
);
|
||||||
|
if (summarizeModel) {
|
||||||
|
return [
|
||||||
|
summarizeModel.name,
|
||||||
|
summarizeModel.provider?.providerName as string,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentModel.startsWith("gemini")) {
|
||||||
|
return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
|
||||||
|
}
|
||||||
|
return [currentModel, providerName];
|
||||||
|
}
|
||||||
|
|
||||||
function countMessages(msgs: ChatMessage[]) {
|
function countMessages(msgs: ChatMessage[]) {
|
||||||
return msgs.reduce(
|
return msgs.reduce(
|
||||||
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
||||||
|
@ -337,22 +372,16 @@ export const useChatStore = createPersistStore(
|
||||||
|
|
||||||
if (attachImages && attachImages.length > 0) {
|
if (attachImages && attachImages.length > 0) {
|
||||||
mContent = [
|
mContent = [
|
||||||
{
|
...(userContent
|
||||||
type: "text",
|
? [{ type: "text" as const, text: userContent }]
|
||||||
text: userContent,
|
: []),
|
||||||
},
|
...attachImages.map((url) => ({
|
||||||
|
type: "image_url" as const,
|
||||||
|
image_url: { url },
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
mContent = mContent.concat(
|
|
||||||
attachImages.map((url) => {
|
|
||||||
return {
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {
|
|
||||||
url: url,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let userMessage: ChatMessage = createMessage({
|
let userMessage: ChatMessage = createMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: mContent,
|
content: mContent,
|
||||||
|
@ -578,8 +607,14 @@ export const useChatStore = createPersistStore(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerName = modelConfig.compressProviderName;
|
// if not config compressModel, then using getSummarizeModel
|
||||||
const api: ClientApi = getClientApi(providerName);
|
const [model, providerName] = modelConfig.compressModel
|
||||||
|
? [modelConfig.compressModel, modelConfig.compressProviderName]
|
||||||
|
: getSummarizeModel(
|
||||||
|
session.mask.modelConfig.model,
|
||||||
|
session.mask.modelConfig.providerName,
|
||||||
|
);
|
||||||
|
const api: ClientApi = getClientApi(providerName as ServiceProvider);
|
||||||
|
|
||||||
// remove error messages if any
|
// remove error messages if any
|
||||||
const messages = session.messages;
|
const messages = session.messages;
|
||||||
|
@ -610,7 +645,7 @@ export const useChatStore = createPersistStore(
|
||||||
api.llm.chat({
|
api.llm.chat({
|
||||||
messages: topicMessages,
|
messages: topicMessages,
|
||||||
config: {
|
config: {
|
||||||
model: modelConfig.compressModel,
|
model,
|
||||||
stream: false,
|
stream: false,
|
||||||
providerName,
|
providerName,
|
||||||
},
|
},
|
||||||
|
@ -674,7 +709,8 @@ export const useChatStore = createPersistStore(
|
||||||
config: {
|
config: {
|
||||||
...modelcfg,
|
...modelcfg,
|
||||||
stream: true,
|
stream: true,
|
||||||
model: modelConfig.compressModel,
|
model,
|
||||||
|
providerName,
|
||||||
},
|
},
|
||||||
onUpdate(message) {
|
onUpdate(message) {
|
||||||
session.memoryPrompt = message;
|
session.memoryPrompt = message;
|
||||||
|
@ -727,7 +763,7 @@ export const useChatStore = createPersistStore(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: StoreKey.Chat,
|
name: StoreKey.Chat,
|
||||||
version: 3.2,
|
version: 3.3,
|
||||||
migrate(persistedState, version) {
|
migrate(persistedState, version) {
|
||||||
const state = persistedState as any;
|
const state = persistedState as any;
|
||||||
const newState = JSON.parse(
|
const newState = JSON.parse(
|
||||||
|
@ -783,6 +819,14 @@ export const useChatStore = createPersistStore(
|
||||||
config.modelConfig.compressProviderName;
|
config.modelConfig.compressProviderName;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// revert default summarize model for every session
|
||||||
|
if (version < 3.3) {
|
||||||
|
newState.sessions.forEach((s) => {
|
||||||
|
const config = useAppConfig.getState();
|
||||||
|
s.mask.modelConfig.compressModel = "";
|
||||||
|
s.mask.modelConfig.compressProviderName = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return newState as any;
|
return newState as any;
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,6 +52,8 @@ export const DEFAULT_CONFIG = {
|
||||||
|
|
||||||
enableArtifacts: true, // show artifacts config
|
enableArtifacts: true, // show artifacts config
|
||||||
|
|
||||||
|
enableCodeFold: true, // code fold config
|
||||||
|
|
||||||
disablePromptHint: false,
|
disablePromptHint: false,
|
||||||
|
|
||||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||||
|
@ -71,8 +73,8 @@ export const DEFAULT_CONFIG = {
|
||||||
sendMemory: true,
|
sendMemory: true,
|
||||||
historyMessageCount: 4,
|
historyMessageCount: 4,
|
||||||
compressMessageLengthThreshold: 1000,
|
compressMessageLengthThreshold: 1000,
|
||||||
compressModel: "gpt-4o-mini" as ModelType,
|
compressModel: "",
|
||||||
compressProviderName: "OpenAI" as ServiceProvider,
|
compressProviderName: "",
|
||||||
enableInjectSystemPrompts: true,
|
enableInjectSystemPrompts: true,
|
||||||
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||||
size: "1024x1024" as DalleSize,
|
size: "1024x1024" as DalleSize,
|
||||||
|
@ -178,7 +180,7 @@ export const useAppConfig = createPersistStore(
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Config,
|
name: StoreKey.Config,
|
||||||
version: 4,
|
version: 4.1,
|
||||||
|
|
||||||
merge(persistedState, currentState) {
|
merge(persistedState, currentState) {
|
||||||
const state = persistedState as ChatConfig | undefined;
|
const state = persistedState as ChatConfig | undefined;
|
||||||
|
@ -231,7 +233,7 @@ export const useAppConfig = createPersistStore(
|
||||||
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version < 4) {
|
if (version < 4.1) {
|
||||||
state.modelConfig.compressModel =
|
state.modelConfig.compressModel =
|
||||||
DEFAULT_CONFIG.modelConfig.compressModel;
|
DEFAULT_CONFIG.modelConfig.compressModel;
|
||||||
state.modelConfig.compressProviderName =
|
state.modelConfig.compressProviderName =
|
||||||
|
|
|
@ -19,6 +19,7 @@ export type Mask = {
|
||||||
builtin: boolean;
|
builtin: boolean;
|
||||||
plugin?: string[];
|
plugin?: string[];
|
||||||
enableArtifacts?: boolean;
|
enableArtifacts?: boolean;
|
||||||
|
enableCodeFold?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MASK_STATE = {
|
export const DEFAULT_MASK_STATE = {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import yaml from "js-yaml";
|
||||||
import { adapter, getOperationId } from "../utils";
|
import { adapter, getOperationId } from "../utils";
|
||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
|
|
||||||
const isApp = getClientConfig()?.isApp;
|
const isApp = getClientConfig()?.isApp !== false;
|
||||||
|
|
||||||
export type Plugin = {
|
export type Plugin = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -151,7 +151,7 @@ export const usePromptStore = createPersistStore(
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROMPT_URL = "./prompts.json";
|
const PROMPT_URL = "./prompts.json";
|
||||||
|
|
||||||
type PromptList = Array<[string, string]>;
|
type PromptList = Array<[string, string]>;
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { downloadAs, readFromFile } from "../utils";
|
||||||
import { showToast } from "../components/ui-lib";
|
import { showToast } from "../components/ui-lib";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { createSyncClient, ProviderType } from "../utils/cloud";
|
import { createSyncClient, ProviderType } from "../utils/cloud";
|
||||||
import { corsPath } from "../utils/cors";
|
|
||||||
|
|
||||||
export interface WebDavConfig {
|
export interface WebDavConfig {
|
||||||
server: string;
|
server: string;
|
||||||
|
@ -26,7 +25,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||||
const DEFAULT_SYNC_STATE = {
|
const DEFAULT_SYNC_STATE = {
|
||||||
provider: ProviderType.WebDAV,
|
provider: ProviderType.WebDAV,
|
||||||
useProxy: true,
|
useProxy: true,
|
||||||
proxyUrl: corsPath(ApiPath.Cors),
|
proxyUrl: ApiPath.Cors as string,
|
||||||
|
|
||||||
webdav: {
|
webdav: {
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
|
import { clientUpdate } from "../utils";
|
||||||
import ChatGptIcon from "../icons/chatgpt.png";
|
import ChatGptIcon from "../icons/chatgpt.png";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { ClientApi } from "../client/api";
|
import { ClientApi } from "../client/api";
|
||||||
|
@ -119,6 +120,7 @@ export const useUpdateStore = createPersistStore(
|
||||||
icon: `${ChatGptIcon.src}`,
|
icon: `${ChatGptIcon.src}`,
|
||||||
sound: "Default",
|
sound: "Default",
|
||||||
});
|
});
|
||||||
|
clientUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
65
app/utils.ts
65
app/utils.ts
|
@ -2,8 +2,9 @@ import { useEffect, useState } from "react";
|
||||||
import { showToast } from "./components/ui-lib";
|
import { showToast } from "./components/ui-lib";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
import { RequestMessage } from "./client/api";
|
import { RequestMessage } from "./client/api";
|
||||||
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
|
import { ServiceProvider } from "./constant";
|
||||||
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
// import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
||||||
|
import { fetch as tauriStreamFetch } from "./utils/stream";
|
||||||
|
|
||||||
export function trimTopic(topic: string) {
|
export function trimTopic(topic: string) {
|
||||||
// Fix an issue where double quotes still show in the Indonesian language
|
// Fix an issue where double quotes still show in the Indonesian language
|
||||||
|
@ -284,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
|
||||||
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (provider == ServiceProvider.Google && !model.includes("vision")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,30 +296,23 @@ export function fetch(
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (window.__TAURI__) {
|
if (window.__TAURI__) {
|
||||||
const payload = options?.body || options?.data;
|
return tauriStreamFetch(url, options);
|
||||||
return tauriFetch(url, {
|
|
||||||
...options,
|
|
||||||
body:
|
|
||||||
payload &&
|
|
||||||
({
|
|
||||||
type: "Text",
|
|
||||||
payload,
|
|
||||||
} as any),
|
|
||||||
timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
|
|
||||||
responseType:
|
|
||||||
options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
|
|
||||||
} as any);
|
|
||||||
}
|
}
|
||||||
return window.fetch(url, options);
|
return window.fetch(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adapter(config: Record<string, unknown>) {
|
export function adapter(config: Record<string, unknown>) {
|
||||||
const { baseURL, url, params, ...rest } = config;
|
const { baseURL, url, params, data: body, ...rest } = config;
|
||||||
const path = baseURL ? `${baseURL}${url}` : url;
|
const path = baseURL ? `${baseURL}${url}` : url;
|
||||||
const fetchUrl = params
|
const fetchUrl = params
|
||||||
? `${path}?${new URLSearchParams(params as any).toString()}`
|
? `${path}?${new URLSearchParams(params as any).toString()}`
|
||||||
: path;
|
: path;
|
||||||
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
|
return fetch(fetchUrl as string, { ...rest, body }).then((res) => {
|
||||||
|
const { status, headers, statusText } = res;
|
||||||
|
return res
|
||||||
|
.text()
|
||||||
|
.then((data: string) => ({ status, statusText, headers, data }));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeLocalStorage(): {
|
export function safeLocalStorage(): {
|
||||||
|
@ -389,3 +386,37 @@ export function getOperationId(operation: {
|
||||||
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
|
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clientUpdate() {
|
||||||
|
// this a wild for updating client app
|
||||||
|
return window.__TAURI__?.updater
|
||||||
|
.checkUpdate()
|
||||||
|
.then((updateResult) => {
|
||||||
|
if (updateResult.shouldUpdate) {
|
||||||
|
window.__TAURI__?.updater
|
||||||
|
.installUpdate()
|
||||||
|
.then((result) => {
|
||||||
|
showToast(Locale.Settings.Update.Success);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("[Install Update Error]", e);
|
||||||
|
showToast(Locale.Settings.Update.Failed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("[Check Update Error]", e);
|
||||||
|
showToast(Locale.Settings.Update.Failed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
|
||||||
|
export function semverCompare(a: string, b: string) {
|
||||||
|
if (a.startsWith(b + "-")) return -1;
|
||||||
|
if (b.startsWith(a + "-")) return 1;
|
||||||
|
return a.localeCompare(b, undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: "case",
|
||||||
|
caseFirst: "upper",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
fetchEventSource,
|
fetchEventSource,
|
||||||
} from "@fortaine/fetch-event-source";
|
} from "@fortaine/fetch-event-source";
|
||||||
import { prettyObject } from "./format";
|
import { prettyObject } from "./format";
|
||||||
|
import { fetch as tauriFetch } from "./stream";
|
||||||
|
|
||||||
export function compressImage(file: Blob, maxSize: number): Promise<string> {
|
export function compressImage(file: Blob, maxSize: number): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -221,7 +222,12 @@ export function stream(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const content = JSON.stringify(res.data);
|
let content = res.data || res?.statusText;
|
||||||
|
// hotfix #5614
|
||||||
|
content =
|
||||||
|
typeof content === "string"
|
||||||
|
? content
|
||||||
|
: JSON.stringify(content);
|
||||||
if (res.status >= 300) {
|
if (res.status >= 300) {
|
||||||
return Promise.reject(content);
|
return Promise.reject(content);
|
||||||
}
|
}
|
||||||
|
@ -236,10 +242,15 @@ export function stream(
|
||||||
return content;
|
return content;
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
options?.onAfterTool?.({ ...tool, isError: true });
|
options?.onAfterTool?.({
|
||||||
|
...tool,
|
||||||
|
isError: true,
|
||||||
|
errorMsg: e.toString(),
|
||||||
|
});
|
||||||
return e.toString();
|
return e.toString();
|
||||||
})
|
})
|
||||||
.then((content) => ({
|
.then((content) => ({
|
||||||
|
name: tool.function.name,
|
||||||
role: "tool",
|
role: "tool",
|
||||||
content,
|
content,
|
||||||
tool_call_id: tool.id,
|
tool_call_id: tool.id,
|
||||||
|
@ -287,6 +298,7 @@ export function stream(
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
|
fetch: tauriFetch as any,
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { getClientConfig } from "../config/client";
|
|
||||||
import { DEFAULT_API_HOST } from "../constant";
|
|
||||||
|
|
||||||
export function corsPath(path: string) {
|
|
||||||
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : "";
|
|
||||||
|
|
||||||
if (baseUrl === "" && path === "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (!path.startsWith("/")) {
|
|
||||||
path = "/" + path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path.endsWith("/")) {
|
|
||||||
path += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${baseUrl}${path}`;
|
|
||||||
}
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
// using tauri command to send request
|
||||||
|
// see src-tauri/src/stream.rs, and src-tauri/src/main.rs
|
||||||
|
// 1. invoke('stream_fetch', {url, method, headers, body}), get response with headers.
|
||||||
|
// 2. listen event: `stream-response` multi times to get body
|
||||||
|
|
||||||
|
type ResponseEvent = {
|
||||||
|
id: number;
|
||||||
|
payload: {
|
||||||
|
request_id: number;
|
||||||
|
status?: number;
|
||||||
|
chunk?: number[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamResponse = {
|
||||||
|
request_id: number;
|
||||||
|
status: number;
|
||||||
|
status_text: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetch(url: string, options?: RequestInit): Promise<any> {
|
||||||
|
if (window.__TAURI__) {
|
||||||
|
const {
|
||||||
|
signal,
|
||||||
|
method = "GET",
|
||||||
|
headers: _headers = {},
|
||||||
|
body = [],
|
||||||
|
} = options || {};
|
||||||
|
let unlisten: Function | undefined;
|
||||||
|
let setRequestId: Function | undefined;
|
||||||
|
const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
|
||||||
|
const ts = new TransformStream();
|
||||||
|
const writer = ts.writable.getWriter();
|
||||||
|
|
||||||
|
let closed = false;
|
||||||
|
const close = () => {
|
||||||
|
if (closed) return;
|
||||||
|
closed = true;
|
||||||
|
unlisten && unlisten();
|
||||||
|
writer.ready.then(() => {
|
||||||
|
writer.close().catch((e) => console.error(e));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
signal.addEventListener("abort", () => close());
|
||||||
|
}
|
||||||
|
// @ts-ignore 2. listen response multi times, and write to Response.body
|
||||||
|
window.__TAURI__.event
|
||||||
|
.listen("stream-response", (e: ResponseEvent) =>
|
||||||
|
requestIdPromise.then((request_id) => {
|
||||||
|
const { request_id: rid, chunk, status } = e?.payload || {};
|
||||||
|
if (request_id != rid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (chunk) {
|
||||||
|
writer.ready.then(() => {
|
||||||
|
writer.write(new Uint8Array(chunk));
|
||||||
|
});
|
||||||
|
} else if (status === 0) {
|
||||||
|
// end of body
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((u: Function) => (unlisten = u));
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Accept: "application/json, text/plain, */*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
|
||||||
|
"User-Agent": navigator.userAgent,
|
||||||
|
};
|
||||||
|
for (const item of new Headers(_headers || {})) {
|
||||||
|
headers[item[0]] = item[1];
|
||||||
|
}
|
||||||
|
return window.__TAURI__
|
||||||
|
.invoke("stream_fetch", {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
// TODO FormData
|
||||||
|
body:
|
||||||
|
typeof body === "string"
|
||||||
|
? Array.from(new TextEncoder().encode(body))
|
||||||
|
: [],
|
||||||
|
})
|
||||||
|
.then((res: StreamResponse) => {
|
||||||
|
const { request_id, status, status_text: statusText, headers } = res;
|
||||||
|
setRequestId?.(request_id);
|
||||||
|
const response = new Response(ts.readable, {
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
if (status >= 300) {
|
||||||
|
setTimeout(close, 100);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("stream error", e);
|
||||||
|
// throw e;
|
||||||
|
return new Response("", { status: 599 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return window.fetch(url, options);
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Config } from "jest";
|
||||||
|
import nextJest from "next/jest.js";
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||||
|
dir: "./",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any custom config to be passed to Jest
|
||||||
|
const config: Config = {
|
||||||
|
coverageProvider: "v8",
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"],
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^@/(.*)$": "<rootDir>/$1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
export default createJestConfig(config);
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import "@testing-library/jest-dom";
|
12
package.json
12
package.json
|
@ -15,7 +15,9 @@
|
||||||
"app:build": "yarn mask && yarn tauri build",
|
"app:build": "yarn mask && yarn tauri build",
|
||||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||||
|
"test": "jest --watch",
|
||||||
|
"test:ci": "jest --ci"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortaine/fetch-event-source": "^3.0.6",
|
"@fortaine/fetch-event-source": "^3.0.6",
|
||||||
|
@ -54,6 +56,9 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
|
"@testing-library/jest-dom": "^6.4.8",
|
||||||
|
"@testing-library/react": "^16.0.0",
|
||||||
|
"@types/jest": "^29.5.13",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
|
@ -69,8 +74,11 @@
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^3.2.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^13.2.2",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.0.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.16.0",
|
"tsx": "^4.16.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"watch": "^1.0.2",
|
"watch": "^1.0.2",
|
||||||
|
@ -80,4 +88,4 @@
|
||||||
"lint-staged/yaml": "^2.2.2"
|
"lint-staged/yaml": "^2.2.2"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19"
|
"packageManager": "yarn@1.22.19"
|
||||||
}
|
}
|
||||||
|
|
|
@ -348,9 +348,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.4.0"
|
version = "1.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -942,9 +942,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.1.0"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
|
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
@ -970,9 +970,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
|
@ -987,9 +987,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
|
@ -1008,9 +1008,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1019,21 +1019,21 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.29"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.28"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
|
@ -1555,9 +1555,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.3.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
|
@ -1986,6 +1986,10 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||||
name = "nextchat"
|
name = "nextchat"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"percent-encoding",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
@ -2281,9 +2285,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.2.0"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
|
@ -2545,9 +2549,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.58"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
|
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -3889,9 +3893,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.3.1"
|
version = "2.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
|
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
|
|
|
@ -37,6 +37,10 @@ tauri = { version = "1.5.4", features = [ "http-all",
|
||||||
"window-unminimize",
|
"window-unminimize",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
|
percent-encoding = "2.3.1"
|
||||||
|
reqwest = "0.11.18"
|
||||||
|
futures-util = "0.3.30"
|
||||||
|
bytes = "1.7.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod stream;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![stream::stream_fetch])
|
||||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use futures_util::{StreamExt};
|
||||||
|
use reqwest::Client;
|
||||||
|
use reqwest::header::{HeaderName, HeaderMap};
|
||||||
|
|
||||||
|
static REQUEST_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct StreamResponse {
|
||||||
|
request_id: u32,
|
||||||
|
status: u16,
|
||||||
|
status_text: String,
|
||||||
|
headers: HashMap<String, String>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize)]
|
||||||
|
pub struct EndPayload {
|
||||||
|
request_id: u32,
|
||||||
|
status: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize)]
|
||||||
|
pub struct ChunkPayload {
|
||||||
|
request_id: u32,
|
||||||
|
chunk: bytes::Bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stream_fetch(
|
||||||
|
window: tauri::Window,
|
||||||
|
method: String,
|
||||||
|
url: String,
|
||||||
|
headers: HashMap<String, String>,
|
||||||
|
body: Vec<u8>,
|
||||||
|
) -> Result<StreamResponse, String> {
|
||||||
|
|
||||||
|
let event_name = "stream-response";
|
||||||
|
let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let mut _headers = HeaderMap::new();
|
||||||
|
for (key, value) in &headers {
|
||||||
|
_headers.insert(key.parse::<HeaderName>().unwrap(), value.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!("method: {:?}", method);
|
||||||
|
// println!("url: {:?}", url);
|
||||||
|
// println!("headers: {:?}", headers);
|
||||||
|
// println!("headers: {:?}", _headers);
|
||||||
|
|
||||||
|
let method = method.parse::<reqwest::Method>().map_err(|err| format!("failed to parse method: {}", err))?;
|
||||||
|
let client = Client::builder()
|
||||||
|
.default_headers(_headers)
|
||||||
|
.redirect(reqwest::redirect::Policy::limited(3))
|
||||||
|
.connect_timeout(Duration::new(3, 0))
|
||||||
|
.build()
|
||||||
|
.map_err(|err| format!("failed to generate client: {}", err))?;
|
||||||
|
|
||||||
|
let mut request = client.request(
|
||||||
|
method.clone(),
|
||||||
|
url.parse::<reqwest::Url>().map_err(|err| format!("failed to parse url: {}", err))?
|
||||||
|
);
|
||||||
|
|
||||||
|
if method == reqwest::Method::POST || method == reqwest::Method::PUT || method == reqwest::Method::PATCH {
|
||||||
|
let body = bytes::Bytes::from(body);
|
||||||
|
// println!("body: {:?}", body);
|
||||||
|
request = request.body(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!("client: {:?}", client);
|
||||||
|
// println!("request: {:?}", request);
|
||||||
|
|
||||||
|
let response_future = request.send();
|
||||||
|
|
||||||
|
let res = response_future.await;
|
||||||
|
let response = match res {
|
||||||
|
Ok(res) => {
|
||||||
|
// get response and emit to client
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
for (name, value) in res.headers() {
|
||||||
|
headers.insert(
|
||||||
|
name.as_str().to_string(),
|
||||||
|
std::str::from_utf8(value.as_bytes()).unwrap().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let status = res.status().as_u16();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let mut stream = res.bytes_stream();
|
||||||
|
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
match chunk {
|
||||||
|
Ok(bytes) => {
|
||||||
|
// println!("chunk: {:?}", bytes);
|
||||||
|
if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }) {
|
||||||
|
println!("Failed to emit chunk payload: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Error chunk: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) {
|
||||||
|
println!("Failed to emit end payload: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
StreamResponse {
|
||||||
|
request_id,
|
||||||
|
status,
|
||||||
|
status_text: "OK".to_string(),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
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 {
|
||||||
|
request_id,
|
||||||
|
status: 599,
|
||||||
|
status_text: "Error".to_string(),
|
||||||
|
headers: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// println!("Response: {:?}", response);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "NextChat",
|
"productName": "NextChat",
|
||||||
"version": "2.15.3"
|
"version": "2.15.6"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
|
"https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
|
||||||
],
|
],
|
||||||
"dialog": false,
|
"dialog": true,
|
||||||
"windows": {
|
"windows": {
|
||||||
"installMode": "passive"
|
"installMode": "passive"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
function sum(a: number, b: number) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("sum module", () => {
|
||||||
|
test("adds 1 + 2 to equal 3", () => {
|
||||||
|
expect(sum(1, 2)).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue