From 4d6b981a54d676a4d70b767418ec842bb4f3114a Mon Sep 17 00:00:00 2001 From: butterfly Date: Tue, 26 Mar 2024 11:43:55 +0800 Subject: [PATCH 01/54] bugfix: Delete the escapeDollarNumber function, which causes errors in rendering a latex string --- app/components/markdown.tsx | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 7c70fe1a5..c6290d8e0 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -99,23 +99,6 @@ export function PreCode(props: { children: any }) { ); } -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) { const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g; @@ -136,7 +119,7 @@ function escapeBrackets(text: string) { function _MarkDownContent(props: { content: string }) { const escapedContent = useMemo( - () => escapeBrackets(escapeDollarNumber(props.content)), + () => escapeBrackets(props.content), [props.content], ); From b7892b58f5560ca7bf10afa6d293d8db4daf3c16 Mon Sep 17 00:00:00 2001 From: river Date: Wed, 25 Sep 2024 13:34:04 +0800 Subject: [PATCH 02/54] chore: support saas --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2001d4d88..c72c791b2 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,18 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。 +[![Saas][Saas-image]][saas-url] [![Web][Web-image]][web-url] [![Windows][Windows-image]][download-url] [![MacOS][MacOS-image]][download-url] [![Linux][Linux-image]][download-url] -[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev) +[NextChatAI](https://nextchat.dev/chat) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev) -[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) +[NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) +[saas-url]: https://nextchat.dev/chat +[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge [web-url]: https://app.nextchat.dev/ [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases [Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge From 6655c64e5502d3ad845758e35c028f8c1df5bc25 Mon Sep 17 00:00:00 2001 From: river Date: Wed, 25 Sep 2024 16:29:59 +0800 Subject: [PATCH 03/54] chore: cn --- README_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index 7831e2ee9..c5d02477c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,7 +8,7 @@ 一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。 -[企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) /[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) +[NextChatAI](https://nextchat.dev/chat) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) [Deploy on Zeabur](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) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) From 13777786c45b37eb2b2081bebaa3a406e55d1959 Mon Sep 17 00:00:00 2001 From: river Date: Wed, 25 Sep 2024 16:30:26 +0800 Subject: [PATCH 04/54] chore: ja --- README_JA.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_JA.md b/README_JA.md index 1716089af..2b0a3ab78 100644 --- a/README_JA.md +++ b/README_JA.md @@ -5,7 +5,7 @@ ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。 -[企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N) +[NextChatAI](https://nextchat.dev/chat) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N) [Zeaburでデプロイ](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) [Zeaburでデプロイ](https://zeabur.com/templates/ZBUEFA) [Gitpodで開く](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) From 702e17c96b2c533cdb9e0589d19601d57e9abb8b Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Thu, 26 Sep 2024 23:21:42 +0800 Subject: [PATCH 05/54] google api using `x-google-api-key` header --- app/api/google.ts | 5 ++++- app/client/api.ts | 15 +++++++++++---- app/client/platforms/google.ts | 4 ---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/api/google.ts b/app/api/google.ts index e6ab47256..396237eea 100644 --- a/app/api/google.ts +++ b/app/api/google.ts @@ -91,7 +91,7 @@ async function request(req: NextRequest, apiKey: string) { }, 10 * 60 * 1000, ); - const fetchUrl = `${baseUrl}${path}?key=${apiKey}${ + const fetchUrl = `${baseUrl}${path}${ req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : "" }`; @@ -100,6 +100,9 @@ async function request(req: NextRequest, apiKey: string) { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", + "x-google-api-key": + req.headers.get("x-google-api-key") || + (req.headers.get("Authorization") ?? "").replace("Bearer "), }, method: req.method, body: req.body, diff --git a/app/client/api.ts b/app/client/api.ts index 8285b4d9f..48bbde6bc 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -272,7 +272,13 @@ export function getHeaders(ignoreHeaders: boolean = false) { } function getAuthHeader(): string { - return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization"; + return isAzure + ? "api-key" + : isAnthropic + ? "x-api-key" + : isGoogle + ? "x-goog-api-key" + : "Authorization"; } const { @@ -283,14 +289,15 @@ export function getHeaders(ignoreHeaders: boolean = false) { apiKey, isEnabledAccessControl, } = getConfig(); - // when using google api in app, not set auth header - if (isGoogle && clientConfig?.isApp) return headers; // when using baidu api in app, not set auth header if (isBaidu && clientConfig?.isApp) return headers; const authHeader = getAuthHeader(); - const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic); + const bearerToken = getBearerToken( + apiKey, + isAzure || isAnthropic || isGoogle, + ); if (bearerToken) { headers[authHeader] = bearerToken; diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index ecb5ce44b..3c2607271 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -48,10 +48,6 @@ export class GeminiProApi implements LLMApi { let chatPath = [baseUrl, path].join("/"); chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse"; - // if chatPath.startsWith('http') then add key in query string - if (chatPath.startsWith("http") && accessStore.googleApiKey) { - chatPath += `&key=${accessStore.googleApiKey}`; - } return chatPath; } extractMessage(res: any) { From 3fb389551ba5284be77734be47b7595c9c425967 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Fri, 27 Sep 2024 11:42:16 +0800 Subject: [PATCH 06/54] fix: build error --- app/api/google.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/google.ts b/app/api/google.ts index 396237eea..7d3f08be4 100644 --- a/app/api/google.ts +++ b/app/api/google.ts @@ -102,7 +102,7 @@ async function request(req: NextRequest, apiKey: string) { "Cache-Control": "no-store", "x-google-api-key": req.headers.get("x-google-api-key") || - (req.headers.get("Authorization") ?? "").replace("Bearer "), + (req.headers.get("Authorization") ?? "").replace("Bearer ", ""), }, method: req.method, body: req.body, From 07d089a2bd41ea74ea9edcd7e1395e6d21e14645 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 27 Sep 2024 13:31:07 +0800 Subject: [PATCH 07/54] try using method and path when operationId is undefined #5525 --- app/store/plugin.ts | 11 ++++++----- app/utils.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/store/plugin.ts b/app/store/plugin.ts index 84ae0816e..a79a1ae4b 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -4,7 +4,7 @@ import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; import { getClientConfig } from "../config/client"; import yaml from "js-yaml"; -import { adapter } from "../utils"; +import { adapter, getOperationId } from "../utils"; import { useAccessStore } from "./access"; const isApp = getClientConfig()?.isApp; @@ -116,7 +116,7 @@ export const FunctionToolService = { return { type: "function", function: { - name: o.operationId, + name: getOperationId(o), description: o.description || o.summary, parameters: parameters, }, @@ -124,7 +124,7 @@ export const FunctionToolService = { }), funcs: operations.reduce((s, o) => { // @ts-ignore - s[o.operationId] = function (args) { + s[getOperationId(o)] = function (args) { const parameters: Record = {}; if (o.parameters instanceof Array) { o.parameters.forEach((p) => { @@ -139,8 +139,8 @@ export const FunctionToolService = { } else if (authLocation == "body") { args[headerName] = tokenValue; } - // @ts-ignore - return api.client[o.operationId]( + // @ts-ignore if o.operationId is null, then using o.path and o.method + return api.client.paths[o.path][o.method]( parameters, args, api.axiosConfigDefaults, @@ -253,6 +253,7 @@ export const usePluginStore = createPersistStore( .catch((e) => item), ), ).then((builtinPlugins: any) => { + return; builtinPlugins .filter((item: any) => item?.content) .forEach((item: any) => { diff --git a/app/utils.ts b/app/utils.ts index 9a8bebf38..6b2f65952 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -377,3 +377,15 @@ export function safeLocalStorage(): { }, }; } + +export function getOperationId(operation: { + operationId?: string; + method: string; + path: string; +}) { + // pattern '^[a-zA-Z0-9_-]+$' + return ( + operation?.operationId || + `${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}` + ); +} From 22aa1698b407322c2cb980aed971ad3d06e6f87c Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Fri, 27 Sep 2024 13:31:49 +0800 Subject: [PATCH 08/54] try using method and path when operationId is undefined #5525 --- app/store/plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/store/plugin.ts b/app/store/plugin.ts index a79a1ae4b..40abdc8d9 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -253,7 +253,6 @@ export const usePluginStore = createPersistStore( .catch((e) => item), ), ).then((builtinPlugins: any) => { - return; builtinPlugins .filter((item: any) => item?.content) .forEach((item: any) => { From 19c4ed4463288fbf3066656197088d88fd35c6ef Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 27 Sep 2024 16:43:50 +0800 Subject: [PATCH 09/54] docs links updated sync.yml https://github.com/Yidadaa/ChatGPT-Next-Web is renamed to https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/ --- .github/workflows/sync.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index e04e30adb..68f5fabec 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -35,6 +35,6 @@ jobs: - name: Sync check if: failure() run: | - echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" - echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" + echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" + echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web#enable-automatic-updates" exit 1 From b35895b551d49cd7448f4311ebb799c0158abcc7 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 27 Sep 2024 16:49:08 +0800 Subject: [PATCH 10/54] Update correct links to manualy code update section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c72c791b2..be5e91d65 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ We recommend that you follow the steps below to re-deploy: ### Enable Automatic Updates -> If you encounter a failure of Upstream Sync execution, please manually sync fork once. +> If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code). After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour: From 2333a47c55f7c840b3f6eacd425deac94d5aceb7 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 27 Sep 2024 16:50:51 +0800 Subject: [PATCH 11/54] Update links in doc to manual code update section (CN) --- README_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index c5d02477c..640fe3933 100644 --- a/README_CN.md +++ b/README_CN.md @@ -54,7 +54,7 @@ ### 打开自动更新 -> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次! +> 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)! 当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新: From c6ebd6e73cbc58bbd752eeab22a3b029985d2e57 Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Fri, 27 Sep 2024 17:00:24 +0800 Subject: [PATCH 12/54] fix: default model --- app/store/access.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/store/access.ts b/app/store/access.ts index d74cb9d02..9fcd227e7 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -211,10 +211,13 @@ export const useAccessStore = createPersistStore( }) .then((res) => res.json()) .then((res) => { - // Set default model from env request - let defaultModel = res.defaultModel ?? ""; - if (defaultModel !== "") - DEFAULT_CONFIG.modelConfig.model = defaultModel; + const defaultModel = res.defaultModel ?? ""; + if (defaultModel !== "") { + const [model, providerName] = defaultModel.split("@"); + DEFAULT_CONFIG.modelConfig.model = model; + DEFAULT_CONFIG.modelConfig.providerName = providerName; + } + return res; }) .then((res: DangerConfig) => { From 2f3457e73d879be322a0ac6cd7d2aa0b44c6542f Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 27 Sep 2024 17:33:02 +0800 Subject: [PATCH 13/54] Update correct links to manualy code update section (JP) --- README_JA.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_JA.md b/README_JA.md index 2b0a3ab78..ba3c514dc 100644 --- a/README_JA.md +++ b/README_JA.md @@ -54,7 +54,7 @@ ### 自動更新を開く -> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください! +> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください! プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります: From 8fb019b2e2a6395ecda0f6d1dd1f2bf47d8ec505 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 27 Sep 2024 17:34:38 +0800 Subject: [PATCH 14/54] revert, leave sync.yml untouched revert commit 19c4ed4463288fbf3066656197088d88fd35c6ef --- .github/workflows/sync.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 68f5fabec..e04e30adb 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -35,6 +35,6 @@ jobs: - name: Sync check if: failure() run: | - echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" - echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web#enable-automatic-updates" + echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" + echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" exit 1 From d84d51b475a1a6ad18efed46121af9adbc7b98f3 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sat, 28 Sep 2024 01:19:39 +0800 Subject: [PATCH 15/54] using sse: schema to fetch in App --- app/utils.ts | 51 +++++++++++++++++-------- src-tauri/Cargo.lock | 86 +++++++++++++++++++++++++++++++------------ src-tauri/Cargo.toml | 4 ++ src-tauri/src/main.rs | 48 ++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 40 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 9a8bebf38..5be7bb2d9 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,8 +2,7 @@ import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; -import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant"; -import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; +import { ServiceProvider } from "./constant"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -292,30 +291,50 @@ export function fetch( options?: Record, ): Promise { if (window.__TAURI__) { - const payload = options?.body || options?.data; - 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); + const tauriUri = window.__TAURI__.convertFileSrc(url, "sse"); + return window.fetch(tauriUri, options).then((r) => { + // 1. create response, + // TODO using event to get status and statusText and headers + const { status, statusText } = r; + const { readable, writable } = new TransformStream(); + const res = new Response(readable, { status, statusText }); + // 2. call fetch_read_body multi times, and write to Response.body + const writer = writable.getWriter(); + let unlisten; + window.__TAURI__.event + .listen("sse-response", (e) => { + const { id, payload } = e; + console.log("event", id, payload); + writer.ready.then(() => { + if (payload !== 0) { + writer.write(new Uint8Array(payload)); + } else { + writer.releaseLock(); + writable.close(); + unlisten && unlisten(); + } + }); + }) + .then((u) => (unlisten = u)); + return res; + }); } return window.fetch(url, options); } +if (undefined !== window) { + window.tauriFetch = fetch; +} + export function adapter(config: Record) { const { baseURL, url, params, ...rest } = config; const path = baseURL ? `${baseURL}${url}` : url; const fetchUrl = params ? `${path}?${new URLSearchParams(params as any).toString()}` : path; - return fetch(fetchUrl as string, { ...rest, responseType: "text" }); + return fetch(fetchUrl as string, rest) + .then((res) => res.text()) + .then((data) => ({ data })); } export function safeLocalStorage(): { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 47d12e119..fcc06d163 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -348,9 +348,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" dependencies = [ "serde", ] @@ -942,9 +942,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -970,9 +970,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" @@ -987,9 +987,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1008,9 +1008,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -1019,21 +1019,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", @@ -1555,9 +1555,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1986,6 +1986,9 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" name = "nextchat" version = "0.1.0" dependencies = [ + "futures-util", + "percent-encoding", + "reqwest", "serde", "serde_json", "tauri", @@ -2213,6 +2216,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_info" +version = "3.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] + [[package]] name = "overload" version = "0.1.1" @@ -2281,9 +2295,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" @@ -2545,9 +2559,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -3237,6 +3251,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sys-locale" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee" +dependencies = [ + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", + "windows-sys 0.45.0", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3385,6 +3412,7 @@ dependencies = [ "objc", "once_cell", "open", + "os_info", "percent-encoding", "rand 0.8.5", "raw-window-handle", @@ -3397,6 +3425,7 @@ dependencies = [ "serde_repr", "serialize-to-javascript", "state", + "sys-locale", "tar", "tauri-macros", "tauri-runtime", @@ -3889,9 +3918,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "url" -version = "2.3.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -4316,6 +4345,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 387584491..31ecfd83e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,8 +35,12 @@ tauri = { version = "1.5.4", features = [ "http-all", "window-start-dragging", "window-unmaximize", "window-unminimize", + "linux-protocol-headers", ] } 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" [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ed3ec32f3..792c656cf 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,9 +1,57 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use futures_util::{StreamExt}; +use reqwest::Client; +use tauri::{ Manager}; +use tauri::http::{ResponseBuilder}; + fn main() { tauri::Builder::default() .plugin(tauri_plugin_window_state::Builder::default().build()) + .register_uri_scheme_protocol("sse", |app_handle, request| { + let path = request.uri().strip_prefix("sse://localhost/").unwrap(); + let path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + // println!("path : {}", path); + let client = Client::new(); + let window = app_handle.get_window("main").unwrap(); + // send http request + let body = reqwest::Body::from(request.body().clone()); + let response_future = client.request(request.method().clone(), path) + .headers(request.headers().clone()) + .body(body).send(); + + // get response and emit to client + tauri::async_runtime::spawn(async move { + let res = response_future.await; + + match res { + Ok(res) => { + let mut stream = res.bytes_stream(); + + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => { + window.emit("sse-response", bytes).unwrap(); + } + Err(err) => { + println!("Error: {:?}", err); + } + } + } + window.emit("sse-response", 0).unwrap(); + } + Err(err) => { + println!("Error: {:?}", err); + } + } + }); + ResponseBuilder::new() + .header("Access-Control-Allow-Origin", "*") + .status(200).body("OK".into()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } From 2d920f7ccc7bed1ed06cdb52e0ef50f96f8100ac Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sat, 28 Sep 2024 15:05:41 +0800 Subject: [PATCH 16/54] using stream: schema to fetch in App --- app/global.d.ts | 1 + app/utils.ts | 41 +--------------- app/utils/stream.ts | 100 ++++++++++++++++++++++++++++++++++++++++ src-tauri/Cargo.lock | 36 +-------------- src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 51 ++------------------ src-tauri/src/stream.rs | 96 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 122 deletions(-) create mode 100644 app/utils/stream.ts create mode 100644 src-tauri/src/stream.rs diff --git a/app/global.d.ts b/app/global.d.ts index 8ee636bcd..a1453dc33 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -12,6 +12,7 @@ declare module "*.svg"; declare interface Window { __TAURI__?: { + convertFileSrc(url: string, protocol?: string): string; writeText(text: string): Promise; invoke(command: string, payload?: Record): Promise; dialog: { diff --git a/app/utils.ts b/app/utils.ts index 5be7bb2d9..fbe77c114 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -3,6 +3,7 @@ import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; import { ServiceProvider } from "./constant"; +import { fetch } from "./utils/stream"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -286,46 +287,6 @@ export function showPlugins(provider: ServiceProvider, model: string) { return false; } -export function fetch( - url: string, - options?: Record, -): Promise { - if (window.__TAURI__) { - const tauriUri = window.__TAURI__.convertFileSrc(url, "sse"); - return window.fetch(tauriUri, options).then((r) => { - // 1. create response, - // TODO using event to get status and statusText and headers - const { status, statusText } = r; - const { readable, writable } = new TransformStream(); - const res = new Response(readable, { status, statusText }); - // 2. call fetch_read_body multi times, and write to Response.body - const writer = writable.getWriter(); - let unlisten; - window.__TAURI__.event - .listen("sse-response", (e) => { - const { id, payload } = e; - console.log("event", id, payload); - writer.ready.then(() => { - if (payload !== 0) { - writer.write(new Uint8Array(payload)); - } else { - writer.releaseLock(); - writable.close(); - unlisten && unlisten(); - } - }); - }) - .then((u) => (unlisten = u)); - return res; - }); - } - return window.fetch(url, options); -} - -if (undefined !== window) { - window.tauriFetch = fetch; -} - export function adapter(config: Record) { const { baseURL, url, params, ...rest } = config; const path = baseURL ? `${baseURL}${url}` : url; diff --git a/app/utils/stream.ts b/app/utils/stream.ts new file mode 100644 index 000000000..8f9ccfbaa --- /dev/null +++ b/app/utils/stream.ts @@ -0,0 +1,100 @@ +// using tauri register_uri_scheme_protocol, register `stream:` protocol +// see src-tauri/src/stream.rs, and src-tauri/src/main.rs +// 1. window.fetch(`stream://localhost/${fetchUrl}`), get request_id +// 2. listen event: `stream-response` multi times to get response headers and body + +type ResponseEvent = { + id: number; + payload: { + request_id: number; + status?: number; + error?: string; + name?: string; + value?: string; + chunk?: number[]; + }; +}; + +export function fetch(url: string, options?: RequestInit): Promise { + if (window.__TAURI__) { + const tauriUri = window.__TAURI__.convertFileSrc(url, "stream"); + const { signal, ...rest } = options || {}; + return window + .fetch(tauriUri, rest) + .then((r) => r.text()) + .then((rid) => parseInt(rid)) + .then((request_id: number) => { + // 1. using event to get status and statusText and headers, and resolve it + let resolve: Function | undefined; + let reject: Function | undefined; + let status: number; + let writable: WritableStream | undefined; + let writer: WritableStreamDefaultWriter | undefined; + const headers = new Headers(); + let unlisten: Function | undefined; + + if (signal) { + signal.addEventListener("abort", () => { + // Reject the promise with the abort reason. + unlisten && unlisten(); + reject && reject(signal.reason); + }); + } + // @ts-ignore 2. listen response multi times, and write to Response.body + window.__TAURI__.event + .listen("stream-response", (e: ResponseEvent) => { + const { id, payload } = e; + const { + request_id: rid, + status: _status, + name, + value, + error, + chunk, + } = payload; + if (request_id != rid) { + return; + } + /** + * 1. get status code + * 2. get headers + * 3. start get body, then resolve response + * 4. get body chunk + */ + if (error) { + unlisten && unlisten(); + return reject && reject(error); + } else if (_status) { + status = _status; + } else if (name && value) { + headers.append(name, value); + } else if (chunk) { + if (resolve) { + const ts = new TransformStream(); + writable = ts.writable; + writer = writable.getWriter(); + resolve(new Response(ts.readable, { status, headers })); + resolve = undefined; + } + writer && + writer.ready.then(() => { + writer && writer.write(new Uint8Array(chunk)); + }); + } else if (_status === 0) { + // end of body + unlisten && unlisten(); + writer && + writer.ready.then(() => { + writer && writer.releaseLock(); + writable && writable.close(); + }); + } + }) + .then((u: Function) => (unlisten = u)); + return new Promise( + (_resolve, _reject) => ([resolve, reject] = [_resolve, _reject]), + ); + }); + } + return window.fetch(url, options); +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fcc06d163..c9baffc0a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1986,6 +1986,7 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" name = "nextchat" version = "0.1.0" dependencies = [ + "bytes", "futures-util", "percent-encoding", "reqwest", @@ -2216,17 +2217,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "os_info" -version = "3.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" -dependencies = [ - "log", - "serde", - "windows-sys 0.52.0", -] - [[package]] name = "overload" version = "0.1.1" @@ -3251,19 +3241,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sys-locale" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee" -dependencies = [ - "js-sys", - "libc", - "wasm-bindgen", - "web-sys", - "windows-sys 0.45.0", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -3412,7 +3389,6 @@ dependencies = [ "objc", "once_cell", "open", - "os_info", "percent-encoding", "rand 0.8.5", "raw-window-handle", @@ -3425,7 +3401,6 @@ dependencies = [ "serde_repr", "serialize-to-javascript", "state", - "sys-locale", "tar", "tauri-macros", "tauri-runtime", @@ -4345,15 +4320,6 @@ dependencies = [ "windows-targets 0.48.0", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", -] - [[package]] name = "windows-targets" version = "0.42.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 31ecfd83e..c954deb72 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,6 +41,7 @@ tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-works percent-encoding = "2.3.1" reqwest = "0.11.18" futures-util = "0.3.30" +bytes = "1.7.2" [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 792c656cf..e38208257 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,57 +1,14 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use futures_util::{StreamExt}; -use reqwest::Client; -use tauri::{ Manager}; -use tauri::http::{ResponseBuilder}; +mod stream; fn main() { tauri::Builder::default() .plugin(tauri_plugin_window_state::Builder::default().build()) - .register_uri_scheme_protocol("sse", |app_handle, request| { - let path = request.uri().strip_prefix("sse://localhost/").unwrap(); - let path = percent_encoding::percent_decode(path.as_bytes()) - .decode_utf8_lossy() - .to_string(); - // println!("path : {}", path); - let client = Client::new(); - let window = app_handle.get_window("main").unwrap(); - // send http request - let body = reqwest::Body::from(request.body().clone()); - let response_future = client.request(request.method().clone(), path) - .headers(request.headers().clone()) - .body(body).send(); - - // get response and emit to client - tauri::async_runtime::spawn(async move { - let res = response_future.await; - - match res { - Ok(res) => { - let mut stream = res.bytes_stream(); - - while let Some(chunk) = stream.next().await { - match chunk { - Ok(bytes) => { - window.emit("sse-response", bytes).unwrap(); - } - Err(err) => { - println!("Error: {:?}", err); - } - } - } - window.emit("sse-response", 0).unwrap(); - } - Err(err) => { - println!("Error: {:?}", err); - } - } - }); - ResponseBuilder::new() - .header("Access-Control-Allow-Origin", "*") - .status(200).body("OK".into()) - }) + .register_uri_scheme_protocol("stream", move |app_handle, request| { + stream::stream(app_handle, request) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs new file mode 100644 index 000000000..5e84e0f00 --- /dev/null +++ b/src-tauri/src/stream.rs @@ -0,0 +1,96 @@ + +use std::error::Error; +use futures_util::{StreamExt}; +use reqwest::Client; +use tauri::{ Manager, AppHandle }; +use tauri::http::{Request, ResponseBuilder}; +use tauri::http::Response; + +static mut REQUEST_COUNTER: u32 = 0; + +#[derive(Clone, serde::Serialize)] +pub struct ErrorPayload { + request_id: u32, + error: String, +} + +#[derive(Clone, serde::Serialize)] +pub struct StatusPayload { + request_id: u32, + status: u16, +} + +#[derive(Clone, serde::Serialize)] +pub struct HeaderPayload { + request_id: u32, + name: String, + value: String, +} + +#[derive(Clone, serde::Serialize)] +pub struct ChunkPayload { + request_id: u32, + chunk: bytes::Bytes, +} + +pub fn stream(app_handle: &AppHandle, request: &Request) -> Result> { + let mut request_id = 0; + let event_name = "stream-response"; + unsafe { + REQUEST_COUNTER += 1; + request_id = REQUEST_COUNTER; + } + let path = request.uri().to_string().replace("stream://localhost/", "").replace("http://stream.localhost/", ""); + let path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + // println!("path : {}", path); + let client = Client::new(); + let handle = app_handle.app_handle(); + // send http request + let body = reqwest::Body::from(request.body().clone()); + let response_future = client.request(request.method().clone(), path) + .headers(request.headers().clone()) + .body(body).send(); + + // get response and emit to client + tauri::async_runtime::spawn(async move { + let res = response_future.await; + + match res { + Ok(res) => { + handle.emit_all(event_name, StatusPayload{ request_id, status: res.status().as_u16() }).unwrap(); + for (name, value) in res.headers() { + handle.emit_all(event_name, HeaderPayload { + request_id, + name: name.to_string(), + value: std::str::from_utf8(value.as_bytes()).unwrap().to_string() + }).unwrap(); + } + let mut stream = res.bytes_stream(); + + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => { + handle.emit_all(event_name, ChunkPayload{ request_id, chunk: bytes }).unwrap(); + } + Err(err) => { + println!("Error: {:?}", err); + } + } + } + handle.emit_all(event_name, StatusPayload { request_id, status: 0 }).unwrap(); + } + Err(err) => { + println!("Error: {:?}", err.source().expect("REASON").to_string()); + handle.emit_all(event_name, ErrorPayload { + request_id, + error: err.source().expect("REASON").to_string() + }).unwrap(); + } + } + }); + return ResponseBuilder::new() + .header("Access-Control-Allow-Origin", "*") + .status(200).body(request_id.to_string().into()) +} From 5bdf41139917f815143de11a9e04b261666c7707 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sun, 29 Sep 2024 15:51:28 +0800 Subject: [PATCH 17/54] hotfix for `x-goog-api-key` --- app/api/google.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/api/google.ts b/app/api/google.ts index 7d3f08be4..707892c33 100644 --- a/app/api/google.ts +++ b/app/api/google.ts @@ -23,7 +23,8 @@ export async function handle( }); } - const bearToken = req.headers.get("Authorization") ?? ""; + const bearToken = + req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || ""; const token = bearToken.trim().replaceAll("Bearer ", "").trim(); const apiKey = token ? token : serverConfig.googleApiKey; @@ -92,7 +93,7 @@ async function request(req: NextRequest, apiKey: string) { 10 * 60 * 1000, ); const fetchUrl = `${baseUrl}${path}${ - req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : "" + req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : "" }`; console.log("[Fetch Url] ", fetchUrl); @@ -100,8 +101,8 @@ async function request(req: NextRequest, apiKey: string) { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", - "x-google-api-key": - req.headers.get("x-google-api-key") || + "x-goog-api-key": + req.headers.get("x-goog-api-key") || (req.headers.get("Authorization") ?? "").replace("Bearer ", ""), }, method: req.method, From 3898c507c466c13537330fd6feb7b960e330c774 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sun, 29 Sep 2024 19:44:09 +0800 Subject: [PATCH 18/54] using stream_fetch in App --- app/utils.ts | 8 ++- app/utils/chat.ts | 2 + app/utils/stream.ts | 154 +++++++++++++++++++--------------------- src-tauri/src/main.rs | 4 +- src-tauri/src/stream.rs | 125 ++++++++++++++++++-------------- 5 files changed, 156 insertions(+), 137 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index fbe77c114..baf45abe5 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -288,12 +288,16 @@ export function showPlugins(provider: ServiceProvider, model: string) { } export function adapter(config: Record) { - const { baseURL, url, params, ...rest } = config; + const { baseURL, url, params, method, data, ...rest } = config; const path = baseURL ? `${baseURL}${url}` : url; const fetchUrl = params ? `${path}?${new URLSearchParams(params as any).toString()}` : path; - return fetch(fetchUrl as string, rest) + return fetch(fetchUrl as string, { + ...rest, + method, + body: method.toUpperCase() == "GET" ? undefined : data, + }) .then((res) => res.text()) .then((data) => ({ data })); } diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 7f3bb23c5..359b2c53e 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -10,6 +10,7 @@ import { fetchEventSource, } from "@fortaine/fetch-event-source"; import { prettyObject } from "./format"; +import { fetch as tauriFetch } from "./stream"; export function compressImage(file: Blob, maxSize: number): Promise { return new Promise((resolve, reject) => { @@ -287,6 +288,7 @@ export function stream( REQUEST_TIMEOUT_MS, ); fetchEventSource(chatPath, { + fetch: tauriFetch, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/utils/stream.ts b/app/utils/stream.ts index 8f9ccfbaa..09b898431 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -1,100 +1,94 @@ -// using tauri register_uri_scheme_protocol, register `stream:` protocol +// using tauri command to send request // see src-tauri/src/stream.rs, and src-tauri/src/main.rs -// 1. window.fetch(`stream://localhost/${fetchUrl}`), get request_id -// 2. listen event: `stream-response` multi times to get response headers and body +// 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; - error?: string; - name?: string; - value?: string; chunk?: number[]; }; }; export function fetch(url: string, options?: RequestInit): Promise { if (window.__TAURI__) { - const tauriUri = window.__TAURI__.convertFileSrc(url, "stream"); - const { signal, ...rest } = options || {}; - return window - .fetch(tauriUri, rest) - .then((r) => r.text()) - .then((rid) => parseInt(rid)) - .then((request_id: number) => { - // 1. using event to get status and statusText and headers, and resolve it - let resolve: Function | undefined; - let reject: Function | undefined; - let status: number; - let writable: WritableStream | undefined; - let writer: WritableStreamDefaultWriter | undefined; - const headers = new Headers(); - let unlisten: Function | undefined; + const { signal, method = "GET", headers = {}, body = [] } = options || {}; + return window.__TAURI__ + .invoke("stream_fetch", { + method, + url, + headers, + // TODO FormData + body: + typeof body === "string" + ? Array.from(new TextEncoder().encode(body)) + : [], + }) + .then( + (res: { + request_id: number; + status: number; + status_text: string; + headers: Record; + }) => { + const { request_id, status, status_text: statusText, headers } = res; + console.log("send request_id", request_id, status, statusText); + let unlisten: Function | undefined; + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); - if (signal) { - signal.addEventListener("abort", () => { - // Reject the promise with the abort reason. + const close = () => { unlisten && unlisten(); - reject && reject(signal.reason); - }); - } - // @ts-ignore 2. listen response multi times, and write to Response.body - window.__TAURI__.event - .listen("stream-response", (e: ResponseEvent) => { - const { id, payload } = e; - const { - request_id: rid, - status: _status, - name, - value, - error, - chunk, - } = payload; - if (request_id != rid) { - return; - } - /** - * 1. get status code - * 2. get headers - * 3. start get body, then resolve response - * 4. get body chunk - */ - if (error) { - unlisten && unlisten(); - return reject && reject(error); - } else if (_status) { - status = _status; - } else if (name && value) { - headers.append(name, value); - } else if (chunk) { - if (resolve) { - const ts = new TransformStream(); - writable = ts.writable; - writer = writable.getWriter(); - resolve(new Response(ts.readable, { status, headers })); - resolve = undefined; + writer.ready.then(() => { + try { + writer.releaseLock(); + } catch (e) { + console.error(e); } - writer && - writer.ready.then(() => { - writer && writer.write(new Uint8Array(chunk)); - }); - } else if (_status === 0) { - // end of body - unlisten && unlisten(); - writer && - writer.ready.then(() => { - writer && writer.releaseLock(); - writable && writable.close(); - }); - } - }) - .then((u: Function) => (unlisten = u)); - return new Promise( - (_resolve, _reject) => ([resolve, reject] = [_resolve, _reject]), - ); + ts.writable.close(); + }); + }; + + const response = new Response(ts.readable, { + status, + statusText, + headers, + }); + 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) => { + const { id, payload } = e; + const { request_id: rid, chunk, status } = payload; + if (request_id != rid) { + return; + } + if (chunk) { + writer && + writer.ready.then(() => { + writer && writer.write(new Uint8Array(chunk)); + }); + } else if (status === 0) { + // end of body + close(); + } + }) + .then((u: Function) => (unlisten = u)); + return response; + }, + ) + .catch((e) => { + console.error("stream error", e); + throw e; }); } return window.fetch(url, options); } + +if (undefined !== window) { + window.tauriFetch = fetch; +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e38208257..d04969c04 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,10 +5,8 @@ mod stream; fn main() { tauri::Builder::default() + .invoke_handler(tauri::generate_handler![stream::stream_fetch]) .plugin(tauri_plugin_window_state::Builder::default().build()) - .register_uri_scheme_protocol("stream", move |app_handle, request| { - stream::stream(app_handle, request) - }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 5e84e0f00..514e62298 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -1,30 +1,25 @@ +// +// use std::error::Error; use futures_util::{StreamExt}; use reqwest::Client; -use tauri::{ Manager, AppHandle }; -use tauri::http::{Request, ResponseBuilder}; -use tauri::http::Response; +use reqwest::header::{HeaderName, HeaderMap}; static mut REQUEST_COUNTER: u32 = 0; #[derive(Clone, serde::Serialize)] -pub struct ErrorPayload { - request_id: u32, - error: String, -} - -#[derive(Clone, serde::Serialize)] -pub struct StatusPayload { +pub struct StreamResponse { request_id: u32, status: u16, + status_text: String, + headers: HashMap } #[derive(Clone, serde::Serialize)] -pub struct HeaderPayload { +pub struct EndPayload { request_id: u32, - name: String, - value: String, + status: u16, } #[derive(Clone, serde::Serialize)] @@ -33,64 +28,90 @@ pub struct ChunkPayload { chunk: bytes::Bytes, } -pub fn stream(app_handle: &AppHandle, request: &Request) -> Result> { +use std::collections::HashMap; + +#[derive(serde::Serialize)] +pub struct CustomResponse { + message: String, + other_val: usize, +} + +#[tauri::command] +pub async fn stream_fetch( + window: tauri::Window, + method: String, + url: String, + headers: HashMap, + body: Vec, +) -> Result { + let mut request_id = 0; let event_name = "stream-response"; unsafe { REQUEST_COUNTER += 1; request_id = REQUEST_COUNTER; } - let path = request.uri().to_string().replace("stream://localhost/", "").replace("http://stream.localhost/", ""); - let path = percent_encoding::percent_decode(path.as_bytes()) - .decode_utf8_lossy() - .to_string(); - // println!("path : {}", path); - let client = Client::new(); - let handle = app_handle.app_handle(); - // send http request - let body = reqwest::Body::from(request.body().clone()); - let response_future = client.request(request.method().clone(), path) - .headers(request.headers().clone()) - .body(body).send(); - // get response and emit to client - tauri::async_runtime::spawn(async move { - let res = response_future.await; + let mut _headers = HeaderMap::new(); + for (key, value) in headers { + _headers.insert(key.parse::().unwrap(), value.parse().unwrap()); + } + let body = bytes::Bytes::from(body); - match res { - Ok(res) => { - handle.emit_all(event_name, StatusPayload{ request_id, status: res.status().as_u16() }).unwrap(); - for (name, value) in res.headers() { - handle.emit_all(event_name, HeaderPayload { - request_id, - name: name.to_string(), - value: std::str::from_utf8(value.as_bytes()).unwrap().to_string() - }).unwrap(); - } + let response_future = Client::new().request( + method.parse::().map_err(|err| format!("failed to parse method: {}", err))?, + url.parse::().map_err(|err| format!("failed to parse url: {}", err))? + ).headers(_headers).body(body).send(); + + let res = response_future.await; + let response = match res { + Ok(res) => { + println!("Error: {:?}", res); + // get response and emit to client + // .register_uri_scheme_protocol("stream", move |app_handle, request| { + 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) => { - handle.emit_all(event_name, ChunkPayload{ request_id, chunk: bytes }).unwrap(); + println!("chunk: {:?}", bytes); + window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }).unwrap(); } Err(err) => { println!("Error: {:?}", err); } } } - handle.emit_all(event_name, StatusPayload { request_id, status: 0 }).unwrap(); - } - Err(err) => { - println!("Error: {:?}", err.source().expect("REASON").to_string()); - handle.emit_all(event_name, ErrorPayload { - request_id, - error: err.source().expect("REASON").to_string() - }).unwrap(); + window.emit(event_name, EndPayload { request_id, status: 0 }).unwrap(); + }); + + StreamResponse { + request_id, + status, + status_text: "OK".to_string(), + headers, } } - }); - return ResponseBuilder::new() - .header("Access-Control-Allow-Origin", "*") - .status(200).body(request_id.to_string().into()) + Err(err) => { + println!("Error: {:?}", err.source().expect("REASON").to_string()); + StreamResponse { + request_id, + status: 599, + status_text: err.source().expect("REASON").to_string(), + headers: HashMap::new(), + } + } + }; + Ok(response) } + From 9e6ee50fa6335fe9c405b69e09bb7b6046aff42c Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sun, 29 Sep 2024 20:32:36 +0800 Subject: [PATCH 19/54] using stream_fetch in App --- app/utils/stream.ts | 110 +++++++++++++++++++--------------------- src-tauri/src/stream.rs | 4 +- 2 files changed, 52 insertions(+), 62 deletions(-) diff --git a/app/utils/stream.ts b/app/utils/stream.ts index 09b898431..f8c272e42 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -12,9 +12,55 @@ type ResponseEvent = { }; }; +type StreamResponse = { + request_id: number; + status: number; + status_text: string; + headers: Record; +}; + export function fetch(url: string, options?: RequestInit): Promise { if (window.__TAURI__) { const { signal, method = "GET", headers = {}, body = [] } = options || {}; + let unlisten: Function | undefined; + let request_id = 0; + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + + const close = () => { + unlisten && unlisten(); + writer.ready.then(() => { + try { + writer.releaseLock(); + } catch (e) { + console.error(e); + } + ts.writable.close(); + }); + }; + + 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) => { + const { request_id: rid, chunk, status } = e?.payload || {}; + if (request_id != rid) { + return; + } + if (chunk) { + writer && + writer.ready.then(() => { + writer && writer.write(new Uint8Array(chunk)); + }); + } else if (status === 0) { + // end of body + close(); + } + }) + .then((u: Function) => (unlisten = u)); + return window.__TAURI__ .invoke("stream_fetch", { method, @@ -26,61 +72,11 @@ export function fetch(url: string, options?: RequestInit): Promise { ? Array.from(new TextEncoder().encode(body)) : [], }) - .then( - (res: { - request_id: number; - status: number; - status_text: string; - headers: Record; - }) => { - const { request_id, status, status_text: statusText, headers } = res; - console.log("send request_id", request_id, status, statusText); - let unlisten: Function | undefined; - const ts = new TransformStream(); - const writer = ts.writable.getWriter(); - - const close = () => { - unlisten && unlisten(); - writer.ready.then(() => { - try { - writer.releaseLock(); - } catch (e) { - console.error(e); - } - ts.writable.close(); - }); - }; - - const response = new Response(ts.readable, { - status, - statusText, - headers, - }); - 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) => { - const { id, payload } = e; - const { request_id: rid, chunk, status } = payload; - if (request_id != rid) { - return; - } - if (chunk) { - writer && - writer.ready.then(() => { - writer && writer.write(new Uint8Array(chunk)); - }); - } else if (status === 0) { - // end of body - close(); - } - }) - .then((u: Function) => (unlisten = u)); - return response; - }, - ) + .then((res: StreamResponse) => { + request_id = res.request_id; + const { status, status_text: statusText, headers } = res; + return new Response(ts.readable, { status, statusText, headers }); + }) .catch((e) => { console.error("stream error", e); throw e; @@ -88,7 +84,3 @@ export function fetch(url: string, options?: RequestInit): Promise { } return window.fetch(url, options); } - -if (undefined !== window) { - window.tauriFetch = fetch; -} diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 514e62298..81710c733 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -66,9 +66,7 @@ pub async fn stream_fetch( let res = response_future.await; let response = match res { Ok(res) => { - println!("Error: {:?}", res); // get response and emit to client - // .register_uri_scheme_protocol("stream", move |app_handle, request| { let mut headers = HashMap::new(); for (name, value) in res.headers() { headers.insert( @@ -84,7 +82,7 @@ pub async fn stream_fetch( while let Some(chunk) = stream.next().await { match chunk { Ok(bytes) => { - println!("chunk: {:?}", bytes); + // println!("chunk: {:?}", bytes); window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }).unwrap(); } Err(err) => { From f9d410517030259434c26f7443bacdc32568682b Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sun, 29 Sep 2024 21:47:38 +0800 Subject: [PATCH 20/54] stash code --- app/utils/stream.ts | 22 ++++++++++++++++++++-- src-tauri/src/stream.rs | 30 ++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/app/utils/stream.ts b/app/utils/stream.ts index f8c272e42..dd665e71c 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -21,7 +21,12 @@ type StreamResponse = { export function fetch(url: string, options?: RequestInit): Promise { if (window.__TAURI__) { - const { signal, method = "GET", headers = {}, body = [] } = options || {}; + const { + signal, + method = "GET", + headers: _headers = {}, + body = [], + } = options || {}; let unlisten: Function | undefined; let request_id = 0; const ts = new TransformStream(); @@ -32,10 +37,10 @@ export function fetch(url: string, options?: RequestInit): Promise { writer.ready.then(() => { try { writer.releaseLock(); + ts.writable.close(); } catch (e) { console.error(e); } - ts.writable.close(); }); }; @@ -61,6 +66,19 @@ export function fetch(url: string, options?: RequestInit): Promise { }) .then((u: Function) => (unlisten = u)); + const headers = { + Accept: "*", + Connection: "close", + Origin: "http://localhost:3000", + Referer: "http://localhost:3000/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", + "User-Agent": navigator.userAgent, + }; + for (const item of new Headers(_headers || {})) { + headers[item[0]] = item[1]; + } return window.__TAURI__ .invoke("stream_fetch", { method, diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 81710c733..97989ba7e 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -53,15 +53,33 @@ pub async fn stream_fetch( } let mut _headers = HeaderMap::new(); - for (key, value) in headers { - _headers.insert(key.parse::().unwrap(), value.parse().unwrap()); + for (key, value) in &headers { + _headers.insert(key.parse::().unwrap(), value.parse().unwrap()); } - let body = bytes::Bytes::from(body); - let response_future = Client::new().request( - method.parse::().map_err(|err| format!("failed to parse method: {}", err))?, + println!("method: {:?}", method); + println!("url: {:?}", url); + println!("headers: {:?}", headers); + println!("headers: {:?}", _headers); + + let method = method.parse::().map_err(|err| format!("failed to parse method: {}", err))?; + let client = Client::builder() + .user_agent("Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15") + .default_headers(_headers) + .build() + .map_err(|err| format!("failed to generate client: {}", err))?; + + let mut request = client.request( + method.clone(), url.parse::().map_err(|err| format!("failed to parse url: {}", err))? - ).headers(_headers).body(body).send(); + ); + + if method == reqwest::Method::POST { + let body = bytes::Bytes::from(body); + println!("body: {:?}", body); + request = request.body(body); + } + let response_future = request.send(); let res = response_future.await; let response = match res { From f5ad51a35e5b21122ca40b7dd6723bff33d51edb Mon Sep 17 00:00:00 2001 From: code-october <148516338+code-october@users.noreply.github.com> Date: Sun, 29 Sep 2024 14:29:42 +0000 Subject: [PATCH 21/54] fix quoteEnd extract regex --- app/components/markdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 6e340d065..7d27f1d60 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -252,7 +252,7 @@ function tryWrapHtmlCode(text: string) { }, ) .replace( - /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g, + /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g, (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => { return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match; }, From b5f6e5a5989a36ecb1b5f2b37209f2d4bffc607b Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 00:38:30 +0800 Subject: [PATCH 22/54] update --- app/store/plugin.ts | 2 +- app/utils/stream.ts | 28 ++++++++++------------------ src-tauri/src/stream.rs | 8 +++++--- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/app/store/plugin.ts b/app/store/plugin.ts index 40abdc8d9..b3d9f6d8c 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -7,7 +7,7 @@ import yaml from "js-yaml"; import { adapter, getOperationId } from "../utils"; import { useAccessStore } from "./access"; -const isApp = getClientConfig()?.isApp; +const isApp = getClientConfig()?.isApp !== false; export type Plugin = { id: string; diff --git a/app/utils/stream.ts b/app/utils/stream.ts index dd665e71c..625ecea1f 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -32,15 +32,13 @@ export function fetch(url: string, options?: RequestInit): Promise { const ts = new TransformStream(); const writer = ts.writable.getWriter(); + let closed = false; const close = () => { + if (closed) return; + closed = true; unlisten && unlisten(); writer.ready.then(() => { - try { - writer.releaseLock(); - ts.writable.close(); - } catch (e) { - console.error(e); - } + writer.close().catch((e) => console.error(e)); }); }; @@ -55,10 +53,9 @@ export function fetch(url: string, options?: RequestInit): Promise { return; } if (chunk) { - writer && - writer.ready.then(() => { - writer && writer.write(new Uint8Array(chunk)); - }); + writer.ready.then(() => { + writer.write(new Uint8Array(chunk)); + }); } else if (status === 0) { // end of body close(); @@ -67,13 +64,8 @@ export function fetch(url: string, options?: RequestInit): Promise { .then((u: Function) => (unlisten = u)); const headers = { - Accept: "*", - Connection: "close", - Origin: "http://localhost:3000", - Referer: "http://localhost:3000/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "cross-site", + 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 || {})) { @@ -81,7 +73,7 @@ export function fetch(url: string, options?: RequestInit): Promise { } return window.__TAURI__ .invoke("stream_fetch", { - method, + method: method.toUpperCase(), url, headers, // TODO FormData diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 97989ba7e..a35e2a001 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -8,7 +8,7 @@ use reqwest::header::{HeaderName, HeaderMap}; static mut REQUEST_COUNTER: u32 = 0; -#[derive(Clone, serde::Serialize)] +#[derive(Debug, Clone, serde::Serialize)] pub struct StreamResponse { request_id: u32, status: u16, @@ -66,6 +66,7 @@ pub async fn stream_fetch( let client = Client::builder() .user_agent("Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15") .default_headers(_headers) + .redirect(reqwest::redirect::Policy::limited(3)) .build() .map_err(|err| format!("failed to generate client: {}", err))?; @@ -104,7 +105,7 @@ pub async fn stream_fetch( window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }).unwrap(); } Err(err) => { - println!("Error: {:?}", err); + println!("Error chunk: {:?}", err); } } } @@ -119,7 +120,7 @@ pub async fn stream_fetch( } } Err(err) => { - println!("Error: {:?}", err.source().expect("REASON").to_string()); + println!("Error response: {:?}", err.source().expect("REASON").to_string()); StreamResponse { request_id, status: 599, @@ -128,6 +129,7 @@ pub async fn stream_fetch( } } }; + println!("Response: {:?}", response); Ok(response) } From 5141145e4df26bbb732e2536bdb6ac2739f2a6bd Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 00:58:50 +0800 Subject: [PATCH 23/54] revert plugin runtime using tarui/api/http, not using fetch_stream --- app/utils.ts | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 315695163..6b2f65952 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,8 +2,8 @@ import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; -import { ServiceProvider } from "./constant"; -import { fetch } from "./utils/stream"; +import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant"; +import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -287,19 +287,35 @@ export function showPlugins(provider: ServiceProvider, model: string) { return false; } +export function fetch( + url: string, + options?: Record, +): Promise { + if (window.__TAURI__) { + const payload = options?.body || options?.data; + 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); +} + export function adapter(config: Record) { - const { baseURL, url, params, method, data, ...rest } = config; + const { baseURL, url, params, ...rest } = config; const path = baseURL ? `${baseURL}${url}` : url; const fetchUrl = params ? `${path}?${new URLSearchParams(params as any).toString()}` : path; - return fetch(fetchUrl as string, { - ...rest, - method, - body: method.toUpperCase() == "GET" ? undefined : data, - }) - .then((res) => res.text()) - .then((data) => ({ data })); + return fetch(fetchUrl as string, { ...rest, responseType: "text" }); } export function safeLocalStorage(): { From a50c282d01c24a66c603d57ce47c978f725f8c7d Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 01:19:20 +0800 Subject: [PATCH 24/54] remove DEFAULT_API_HOST --- app/client/platforms/alibaba.ts | 2 + app/client/platforms/anthropic.ts | 6 +-- app/client/platforms/baidu.ts | 2 + app/client/platforms/bytedance.ts | 2 + app/client/platforms/google.ts | 6 ++- app/client/platforms/iflytek.ts | 6 ++- app/client/platforms/moonshot.ts | 4 +- app/client/platforms/openai.ts | 4 +- app/client/platforms/tencent.ts | 8 ++-- app/constant.ts | 1 - app/store/access.ts | 72 +++++++++---------------------- app/store/sync.ts | 3 +- app/utils/cors.ts | 19 -------- 13 files changed, 45 insertions(+), 90 deletions(-) delete mode 100644 app/utils/cors.ts diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index 4ade9ebb9..727e3aebf 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -23,6 +23,7 @@ import { import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import { getMessageTextContent } from "@/app/utils"; +import { fetch } from "@/app/utils/stream"; export interface OpenAIListModelResponse { object: string; @@ -178,6 +179,7 @@ export class QwenApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { + fetch, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 7826838a6..1a83bd53a 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -8,7 +8,7 @@ import { ChatMessageTool, } from "@/app/store"; 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 { preProcessImageContent, stream } from "@/app/utils/chat"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; @@ -388,9 +388,7 @@ export class ClaudeApi implements LLMApi { if (baseUrl.trim().length === 0) { const isApp = !!getClientConfig()?.isApp; - baseUrl = isApp - ? DEFAULT_API_HOST + "/api/proxy/anthropic" - : ApiPath.Anthropic; + baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic; } if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) { diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index c360417c6..4f3294d5e 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -24,6 +24,7 @@ import { import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import { getMessageTextContent } from "@/app/utils"; +import { fetch } from "@/app/utils/stream"; export interface OpenAIListModelResponse { object: string; @@ -197,6 +198,7 @@ export class ErnieApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { + fetch, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index a6e2d426e..279be815f 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -23,6 +23,7 @@ import { import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import { getMessageTextContent } from "@/app/utils"; +import { fetch } from "@/app/utils/stream"; export interface OpenAIListModelResponse { object: string; @@ -165,6 +166,7 @@ export class DoubaoApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { + fetch, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 3c2607271..7a74dd4f3 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -9,7 +9,7 @@ import { } from "../api"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { getClientConfig } from "@/app/config/client"; -import { DEFAULT_API_HOST } from "@/app/constant"; +import { GEMINI_BASE_URL } from "@/app/constant"; import Locale from "../../locales"; import { EventStreamContentType, @@ -22,6 +22,7 @@ import { isVisionModel, } from "@/app/utils"; import { preProcessImageContent } from "@/app/utils/chat"; +import { fetch } from "@/app/utils/stream"; export class GeminiProApi implements LLMApi { path(path: string): string { @@ -34,7 +35,7 @@ export class GeminiProApi implements LLMApi { const isApp = !!getClientConfig()?.isApp; if (baseUrl.length === 0) { - baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google; + baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google; } if (baseUrl.endsWith("/")) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); @@ -213,6 +214,7 @@ export class GeminiProApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { + fetch, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/iflytek.ts b/app/client/platforms/iflytek.ts index 3931672e6..07bfeda58 100644 --- a/app/client/platforms/iflytek.ts +++ b/app/client/platforms/iflytek.ts @@ -1,7 +1,7 @@ "use client"; import { ApiPath, - DEFAULT_API_HOST, + IFLYTEK_BASE_URL, Iflytek, REQUEST_TIMEOUT_MS, } from "@/app/constant"; @@ -22,6 +22,7 @@ import { import { prettyObject } from "@/app/utils/format"; import { getClientConfig } from "@/app/config/client"; import { getMessageTextContent } from "@/app/utils"; +import { fetch } from "@/app/utils/stream"; import { RequestPayload } from "./openai"; @@ -40,7 +41,7 @@ export class SparkApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; const apiPath = ApiPath.Iflytek; - baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath; + baseUrl = isApp ? IFLYTEK_BASE_URL + apiPath : apiPath; } if (baseUrl.endsWith("/")) { @@ -149,6 +150,7 @@ export class SparkApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { + fetch, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index 6b1979745..4570dca97 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -2,7 +2,7 @@ // azure and openai, using same models. so using same LLMApi. import { ApiPath, - DEFAULT_API_HOST, + MOONSHOT_BASE_URL, Moonshot, REQUEST_TIMEOUT_MS, } from "@/app/constant"; @@ -40,7 +40,7 @@ export class MoonshotApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; const apiPath = ApiPath.Moonshot; - baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath; + baseUrl = isApp ? MOONSHOT_BASE_URL + apiPath : apiPath; } if (baseUrl.endsWith("/")) { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 0a8d6203a..0b2d91c99 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -2,7 +2,7 @@ // azure and openai, using same models. so using same LLMApi. import { ApiPath, - DEFAULT_API_HOST, + OPENAI_BASE_URL, DEFAULT_MODELS, OpenaiPath, Azure, @@ -98,7 +98,7 @@ export class ChatGPTApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI; - baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath; + baseUrl = isApp ? OPENAI_BASE_URL + apiPath : apiPath; } if (baseUrl.endsWith("/")) { diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index 3e8f1a459..28f5b56f2 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -1,5 +1,5 @@ "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 { @@ -22,6 +22,7 @@ import mapKeys from "lodash-es/mapKeys"; import mapValues from "lodash-es/mapValues"; import isArray from "lodash-es/isArray"; import isObject from "lodash-es/isObject"; +import { fetch } from "@/app/utils/stream"; export interface OpenAIListModelResponse { object: string; @@ -70,9 +71,7 @@ export class HunyuanApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; - baseUrl = isApp - ? DEFAULT_API_HOST + "/api/proxy/tencent" - : ApiPath.Tencent; + baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent; } if (baseUrl.endsWith("/")) { @@ -179,6 +178,7 @@ export class HunyuanApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { + fetch, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/constant.ts b/app/constant.ts index a54a973da..a06b8f050 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -11,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; 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 ANTHROPIC_BASE_URL = "https://api.anthropic.com"; diff --git a/app/store/access.ts b/app/store/access.ts index 9fcd227e7..74050733c 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -1,9 +1,17 @@ import { - ApiPath, - DEFAULT_API_HOST, GoogleSafetySettingsThreshold, ServiceProvider, StoreKey, + 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, } from "../constant"; import { getHeaders } from "../client/api"; import { getClientConfig } from "../config/client"; @@ -15,46 +23,6 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done const isApp = getClientConfig()?.buildMode === "export"; -const DEFAULT_OPENAI_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/openai" - : ApiPath.OpenAI; - -const DEFAULT_GOOGLE_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/google" - : ApiPath.Google; - -const DEFAULT_ANTHROPIC_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/anthropic" - : ApiPath.Anthropic; - -const DEFAULT_BAIDU_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/baidu" - : ApiPath.Baidu; - -const DEFAULT_BYTEDANCE_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/bytedance" - : ApiPath.ByteDance; - -const DEFAULT_ALIBABA_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/alibaba" - : ApiPath.Alibaba; - -const DEFAULT_TENCENT_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/tencent" - : ApiPath.Tencent; - -const DEFAULT_MOONSHOT_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/moonshot" - : ApiPath.Moonshot; - -const DEFAULT_STABILITY_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/stability" - : ApiPath.Stability; - -const DEFAULT_IFLYTEK_URL = isApp - ? DEFAULT_API_HOST + "/api/proxy/iflytek" - : ApiPath.Iflytek; - const DEFAULT_ACCESS_STATE = { accessCode: "", useCustomConfig: false, @@ -62,7 +30,7 @@ const DEFAULT_ACCESS_STATE = { provider: ServiceProvider.OpenAI, // openai - openaiUrl: DEFAULT_OPENAI_URL, + openaiUrl: OPENAI_BASE_URL, openaiApiKey: "", // azure @@ -71,44 +39,44 @@ const DEFAULT_ACCESS_STATE = { azureApiVersion: "2023-08-01-preview", // google ai studio - googleUrl: DEFAULT_GOOGLE_URL, + googleUrl: GEMINI_BASE_URL, googleApiKey: "", googleApiVersion: "v1", googleSafetySettings: GoogleSafetySettingsThreshold.BLOCK_ONLY_HIGH, // anthropic - anthropicUrl: DEFAULT_ANTHROPIC_URL, + anthropicUrl: ANTHROPIC_BASE_URL, anthropicApiKey: "", anthropicApiVersion: "2023-06-01", // baidu - baiduUrl: DEFAULT_BAIDU_URL, + baiduUrl: BAIDU_BASE_URL, baiduApiKey: "", baiduSecretKey: "", // bytedance - bytedanceUrl: DEFAULT_BYTEDANCE_URL, + bytedanceUrl: BYTEDANCE_BASE_URL, bytedanceApiKey: "", // alibaba - alibabaUrl: DEFAULT_ALIBABA_URL, + alibabaUrl: ALIBABA_BASE_URL, alibabaApiKey: "", // moonshot - moonshotUrl: DEFAULT_MOONSHOT_URL, + moonshotUrl: MOONSHOT_BASE_URL, moonshotApiKey: "", //stability - stabilityUrl: DEFAULT_STABILITY_URL, + stabilityUrl: STABILITY_BASE_URL, stabilityApiKey: "", // tencent - tencentUrl: DEFAULT_TENCENT_URL, + tencentUrl: TENCENT_BASE_URL, tencentSecretKey: "", tencentSecretId: "", // iflytek - iflytekUrl: DEFAULT_IFLYTEK_URL, + iflytekUrl: IFLYTEK_BASE_URL, iflytekApiKey: "", iflytekApiSecret: "", diff --git a/app/store/sync.ts b/app/store/sync.ts index 9db60d5f4..c53a7a82a 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -12,7 +12,6 @@ import { downloadAs, readFromFile } from "../utils"; import { showToast } from "../components/ui-lib"; import Locale from "../locales"; import { createSyncClient, ProviderType } from "../utils/cloud"; -import { corsPath } from "../utils/cors"; export interface WebDavConfig { server: string; @@ -26,7 +25,7 @@ export type SyncStore = GetStoreState; const DEFAULT_SYNC_STATE = { provider: ProviderType.WebDAV, useProxy: true, - proxyUrl: corsPath(ApiPath.Cors), + proxyUrl: ApiPath.Cors, webdav: { endpoint: "", diff --git a/app/utils/cors.ts b/app/utils/cors.ts deleted file mode 100644 index f5e5ce6f0..000000000 --- a/app/utils/cors.ts +++ /dev/null @@ -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}`; -} From 9be58f3eb4217b00073a954262ac4e5b970806f3 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 01:30:20 +0800 Subject: [PATCH 25/54] fix ts error --- app/client/platforms/alibaba.ts | 2 +- app/client/platforms/baidu.ts | 2 +- app/client/platforms/bytedance.ts | 2 +- app/client/platforms/google.ts | 2 +- app/client/platforms/iflytek.ts | 2 +- app/client/platforms/tencent.ts | 2 +- app/store/sync.ts | 2 +- app/utils/chat.ts | 2 +- app/utils/stream.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index 727e3aebf..86229a147 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -179,7 +179,7 @@ export class QwenApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { - fetch, + fetch: fetch as any, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/baidu.ts b/app/client/platforms/baidu.ts index 4f3294d5e..2511a696b 100644 --- a/app/client/platforms/baidu.ts +++ b/app/client/platforms/baidu.ts @@ -198,7 +198,7 @@ export class ErnieApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { - fetch, + fetch: fetch as any, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/bytedance.ts b/app/client/platforms/bytedance.ts index 279be815f..000a9e278 100644 --- a/app/client/platforms/bytedance.ts +++ b/app/client/platforms/bytedance.ts @@ -166,7 +166,7 @@ export class DoubaoApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { - fetch, + fetch: fetch as any, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 7a74dd4f3..dcf300a0f 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -214,7 +214,7 @@ export class GeminiProApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { - fetch, + fetch: fetch as any, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/iflytek.ts b/app/client/platforms/iflytek.ts index 07bfeda58..de638829e 100644 --- a/app/client/platforms/iflytek.ts +++ b/app/client/platforms/iflytek.ts @@ -150,7 +150,7 @@ export class SparkApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { - fetch, + fetch: fetch as any, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index 28f5b56f2..3610fac0a 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -178,7 +178,7 @@ export class HunyuanApi implements LLMApi { controller.signal.onabort = finish; fetchEventSource(chatPath, { - fetch, + fetch: fetch as any, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/store/sync.ts b/app/store/sync.ts index c53a7a82a..8477c1e4b 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -25,7 +25,7 @@ export type SyncStore = GetStoreState; const DEFAULT_SYNC_STATE = { provider: ProviderType.WebDAV, useProxy: true, - proxyUrl: ApiPath.Cors, + proxyUrl: ApiPath.Cors as string, webdav: { endpoint: "", diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 359b2c53e..254cef401 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -288,7 +288,7 @@ export function stream( REQUEST_TIMEOUT_MS, ); fetchEventSource(chatPath, { - fetch: tauriFetch, + fetch: tauriFetch as any, ...chatPayload, async onopen(res) { clearTimeout(requestTimeoutId); diff --git a/app/utils/stream.ts b/app/utils/stream.ts index 625ecea1f..e8850fcba 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -63,7 +63,7 @@ export function fetch(url: string, options?: RequestInit): Promise { }) .then((u: Function) => (unlisten = u)); - const headers = { + const headers: Record = { 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, From 3c01738c29c64c215f0d118a2db0e65c3f51f4de Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 01:37:16 +0800 Subject: [PATCH 26/54] update --- src-tauri/src/stream.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index a35e2a001..9aae3d164 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -57,14 +57,13 @@ pub async fn stream_fetch( _headers.insert(key.parse::().unwrap(), value.parse().unwrap()); } - println!("method: {:?}", method); - println!("url: {:?}", url); - println!("headers: {:?}", headers); - println!("headers: {:?}", _headers); + // println!("method: {:?}", method); + // println!("url: {:?}", url); + // println!("headers: {:?}", headers); + // println!("headers: {:?}", _headers); let method = method.parse::().map_err(|err| format!("failed to parse method: {}", err))?; let client = Client::builder() - .user_agent("Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15") .default_headers(_headers) .redirect(reqwest::redirect::Policy::limited(3)) .build() @@ -77,7 +76,7 @@ pub async fn stream_fetch( if method == reqwest::Method::POST { let body = bytes::Bytes::from(body); - println!("body: {:?}", body); + // println!("body: {:?}", body); request = request.body(body); } let response_future = request.send(); @@ -129,7 +128,7 @@ pub async fn stream_fetch( } } }; - println!("Response: {:?}", response); + // println!("Response: {:?}", response); Ok(response) } From b174a40634d7c2ab809a2ed89ff6fa2fbbe1beb1 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 01:44:27 +0800 Subject: [PATCH 27/54] update --- app/client/platforms/iflytek.ts | 2 +- app/client/platforms/moonshot.ts | 2 +- app/client/platforms/openai.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/client/platforms/iflytek.ts b/app/client/platforms/iflytek.ts index de638829e..55a39d0cc 100644 --- a/app/client/platforms/iflytek.ts +++ b/app/client/platforms/iflytek.ts @@ -41,7 +41,7 @@ export class SparkApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; const apiPath = ApiPath.Iflytek; - baseUrl = isApp ? IFLYTEK_BASE_URL + apiPath : apiPath; + baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath; } if (baseUrl.endsWith("/")) { diff --git a/app/client/platforms/moonshot.ts b/app/client/platforms/moonshot.ts index 4570dca97..e0ef3494f 100644 --- a/app/client/platforms/moonshot.ts +++ b/app/client/platforms/moonshot.ts @@ -40,7 +40,7 @@ export class MoonshotApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; const apiPath = ApiPath.Moonshot; - baseUrl = isApp ? MOONSHOT_BASE_URL + apiPath : apiPath; + baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath; } if (baseUrl.endsWith("/")) { diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 0b2d91c99..a22633611 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -98,7 +98,7 @@ export class ChatGPTApi implements LLMApi { if (baseUrl.length === 0) { const isApp = !!getClientConfig()?.isApp; const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI; - baseUrl = isApp ? OPENAI_BASE_URL + apiPath : apiPath; + baseUrl = isApp ? OPENAI_BASE_URL : apiPath; } if (baseUrl.endsWith("/")) { From af49ed4fdcd81b05c6bc0f11a35af346180134f8 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 01:51:14 +0800 Subject: [PATCH 28/54] update --- app/global.d.ts | 1 - src-tauri/src/stream.rs | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/global.d.ts b/app/global.d.ts index a1453dc33..8ee636bcd 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -12,7 +12,6 @@ declare module "*.svg"; declare interface Window { __TAURI__?: { - convertFileSrc(url: string, protocol?: string): string; writeText(text: string): Promise; invoke(command: string, payload?: Record): Promise; dialog: { diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 9aae3d164..f7d5a7693 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -2,6 +2,7 @@ // use std::error::Error; +use std::collections::HashMap; use futures_util::{StreamExt}; use reqwest::Client; use reqwest::header::{HeaderName, HeaderMap}; @@ -28,14 +29,6 @@ pub struct ChunkPayload { chunk: bytes::Bytes, } -use std::collections::HashMap; - -#[derive(serde::Serialize)] -pub struct CustomResponse { - message: String, - other_val: usize, -} - #[tauri::command] pub async fn stream_fetch( window: tauri::Window, From f42488d4cb1aba73b4632b49b6606efd0b5a378d Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 02:28:19 +0800 Subject: [PATCH 29/54] using stream fetch replace old tauri http fetch --- app/utils.ts | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 6b2f65952..4887a1021 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -2,8 +2,9 @@ import { useEffect, useState } from "react"; import { showToast } from "./components/ui-lib"; import Locale from "./locales"; import { RequestMessage } from "./client/api"; -import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant"; -import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; +import { ServiceProvider } from "./constant"; +// import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; +import { fetch as tauriStreamFetch } from "./utils/stream"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -292,19 +293,22 @@ export function fetch( options?: Record, ): Promise { if (window.__TAURI__) { - const payload = options?.body || options?.data; - 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 tauriStreamFetch(url, options) + .then((res) => res.text()) + .then((data) => ({ data })); + // const payload = options?.body || options?.data; + // 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); } From 8030e71a5aefe551d23b19867fd8738791c0d712 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 02:33:02 +0800 Subject: [PATCH 30/54] update --- src-tauri/src/stream.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index f7d5a7693..51d844305 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -2,12 +2,13 @@ // 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 mut REQUEST_COUNTER: u32 = 0; +static REQUEST_COUNTER: AtomicU32 = AtomicU32::new(0); #[derive(Debug, Clone, serde::Serialize)] pub struct StreamResponse { @@ -38,12 +39,8 @@ pub async fn stream_fetch( body: Vec, ) -> Result { - let mut request_id = 0; let event_name = "stream-response"; - unsafe { - REQUEST_COUNTER += 1; - request_id = REQUEST_COUNTER; - } + let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst); let mut _headers = HeaderMap::new(); for (key, value) in &headers { @@ -72,6 +69,10 @@ pub async fn stream_fetch( // println!("body: {:?}", body); request = request.body(body); } + + // println!("client: {:?}", client); + // println!("request: {:?}", request); + let response_future = request.send(); let res = response_future.await; From ef4665cd8b590531af31c56562afdbeb7fa7d0bd Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 02:57:51 +0800 Subject: [PATCH 31/54] update --- app/utils.ts | 8 ++++---- app/utils/stream.ts | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 4887a1021..9f5dd3150 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -293,9 +293,7 @@ export function fetch( options?: Record, ): Promise { if (window.__TAURI__) { - return tauriStreamFetch(url, options) - .then((res) => res.text()) - .then((data) => ({ data })); + return tauriStreamFetch(url, options); // const payload = options?.body || options?.data; // return tauriFetch(url, { // ...options, @@ -319,7 +317,9 @@ export function adapter(config: Record) { const fetchUrl = params ? `${path}?${new URLSearchParams(params as any).toString()}` : path; - return fetch(fetchUrl as string, { ...rest, responseType: "text" }); + return fetch(fetchUrl as string, { ...rest, responseType: "text" }) + .then((res) => res.text()) + .then((data) => ({ data })); } export function safeLocalStorage(): { diff --git a/app/utils/stream.ts b/app/utils/stream.ts index e8850fcba..2a8c13777 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -85,7 +85,15 @@ export function fetch(url: string, options?: RequestInit): Promise { .then((res: StreamResponse) => { request_id = res.request_id; const { status, status_text: statusText, headers } = res; - return new Response(ts.readable, { status, statusText, headers }); + const response = new Response(ts.readable, { + status, + statusText, + headers, + }); + if (status >= 300) { + setTimeout(close, 50); + } + return response; }) .catch((e) => { console.error("stream error", e); From 6293b95a3b7f6722d5e8971a4937331119cb174f Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 10:13:11 +0800 Subject: [PATCH 32/54] update default api base url --- app/store/access.ts | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/app/store/access.ts b/app/store/access.ts index 74050733c..dec3a7258 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -2,6 +2,7 @@ import { GoogleSafetySettingsThreshold, ServiceProvider, StoreKey, + ApiPath, OPENAI_BASE_URL, ANTHROPIC_BASE_URL, GEMINI_BASE_URL, @@ -23,6 +24,26 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done const isApp = getClientConfig()?.buildMode === "export"; +const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI; + +const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google; + +const DEFAULT_ANTHROPIC_URL = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic; + +const DEFAULT_BAIDU_URL = isApp ? BAIDU_BASE_URL : ApiPath.Baidu; + +const DEFAULT_BYTEDANCE_URL = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance; + +const DEFAULT_ALIBABA_URL = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba; + +const DEFAULT_TENCENT_URL = isApp ? TENCENT_BASE_URL : ApiPath.Tencent; + +const DEFAULT_MOONSHOT_URL = isApp ? MOONSHOT_BASE_URL : ApiPath.Moonshot; + +const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability; + +const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek; + const DEFAULT_ACCESS_STATE = { accessCode: "", useCustomConfig: false, @@ -30,7 +51,7 @@ const DEFAULT_ACCESS_STATE = { provider: ServiceProvider.OpenAI, // openai - openaiUrl: OPENAI_BASE_URL, + openaiUrl: DEFAULT_OPENAI_URL, openaiApiKey: "", // azure @@ -39,44 +60,44 @@ const DEFAULT_ACCESS_STATE = { azureApiVersion: "2023-08-01-preview", // google ai studio - googleUrl: GEMINI_BASE_URL, + googleUrl: DEFAULT_GOOGLE_URL, googleApiKey: "", googleApiVersion: "v1", googleSafetySettings: GoogleSafetySettingsThreshold.BLOCK_ONLY_HIGH, // anthropic - anthropicUrl: ANTHROPIC_BASE_URL, + anthropicUrl: DEFAULT_ANTHROPIC_URL, anthropicApiKey: "", anthropicApiVersion: "2023-06-01", // baidu - baiduUrl: BAIDU_BASE_URL, + baiduUrl: DEFAULT_BAIDU_URL, baiduApiKey: "", baiduSecretKey: "", // bytedance - bytedanceUrl: BYTEDANCE_BASE_URL, + bytedanceUrl: DEFAULT_BYTEDANCE_URL, bytedanceApiKey: "", // alibaba - alibabaUrl: ALIBABA_BASE_URL, + alibabaUrl: DEFAULT_ALIBABA_URL, alibabaApiKey: "", // moonshot - moonshotUrl: MOONSHOT_BASE_URL, + moonshotUrl: DEFAULT_MOONSHOT_URL, moonshotApiKey: "", //stability - stabilityUrl: STABILITY_BASE_URL, + stabilityUrl: DEFAULT_STABILITY_URL, stabilityApiKey: "", // tencent - tencentUrl: TENCENT_BASE_URL, + tencentUrl: DEFAULT_TENCENT_URL, tencentSecretKey: "", tencentSecretId: "", // iflytek - iflytekUrl: IFLYTEK_BASE_URL, + iflytekUrl: DEFAULT_IFLYTEK_URL, iflytekApiKey: "", iflytekApiSecret: "", From b6d9ba93fa101deeb9920f29bee5f675591dacd5 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 10:18:30 +0800 Subject: [PATCH 33/54] update --- src-tauri/src/stream.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 51d844305..938c663e1 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -95,14 +95,18 @@ pub async fn stream_fetch( match chunk { Ok(bytes) => { // println!("chunk: {:?}", bytes); - window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }).unwrap(); + 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); } } } - window.emit(event_name, EndPayload { request_id, status: 0 }).unwrap(); + if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) { + println!("Failed to emit end payload: {:?}", e); + } }); StreamResponse { From edfa6d14eefb339781c839750fb68bbfeb632011 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 10:23:24 +0800 Subject: [PATCH 34/54] update --- src-tauri/src/stream.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 938c663e1..3d21623e6 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -64,7 +64,7 @@ pub async fn stream_fetch( url.parse::().map_err(|err| format!("failed to parse url: {}", err))? ); - if method == reqwest::Method::POST { + 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); From 7173cf21846475455502cf2d6363fff30e3c4600 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 13:07:06 +0800 Subject: [PATCH 35/54] update --- README.md | 2 ++ src-tauri/tauri.conf.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index be5e91d65..5887369ff 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** ## 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.14.0 Now supports Artifacts & SD - 🚀 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,更安全![#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379) - 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins) - 🚀 v2.14.0 现在支持 Artifacts & SD 了。 - 🚀 v2.10.1 现在支持 Gemini Pro 模型。 diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index eb0d411cb..cc137ee8a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.15.3" + "version": "2.15.4" }, "tauri": { "allowlist": { From deb215ccd1adaf14bb55d1f26de4ef4713fd4780 Mon Sep 17 00:00:00 2001 From: lyf <1910527151@qq.com> Date: Mon, 30 Sep 2024 13:23:24 +0800 Subject: [PATCH 36/54] fix readme --- README.md | 21 +++++++++++++-------- README_CN.md | 2 +- README_JA.md | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index be5e91d65..e10d3bce8 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 [![MacOS][MacOS-image]][download-url] [![Linux][Linux-image]][download-url] -[NextChatAI](https://nextchat.dev/chat) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev) +[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev) [NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) -[saas-url]: https://nextchat.dev/chat +[saas-url]: https://nextchat.dev/chat?utm_source=readme [saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge [web-url]: https://app.nextchat.dev/ [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases @@ -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 [Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu -[Deploy on Zeabur](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) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) +[Deploy on Zeabur](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) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [](https://monica.im/?utm=nxcrp) @@ -40,6 +40,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 ## Enterprise Edition Meeting Your Company's Privatization and Customization Deployment Requirements: + - **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image. - **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members. - **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel. @@ -53,6 +54,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** ## 企业版 满足企业用户私有化部署和个性化定制需求: + - **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合 - **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用 - **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制 @@ -101,7 +103,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** ## What's New - 🚀 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.9.11 you can use azure endpoint now. - 🚀 v2.8 now we have a client that runs across all platforms! @@ -132,8 +134,8 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) - [x] 插件机制,支持`联网搜索`、`计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - - [ ] 本地知识库 + - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) +- [ ] 本地知识库 ## 最新动态 @@ -333,10 +335,12 @@ 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. 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. > 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. + > Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list. ### `DEFAULT_MODEL` (optional) @@ -346,8 +350,9 @@ Change default model ### `WHITE_WEBDAV_ENDPOINTS` (optional) You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format: -- Each address must be a complete endpoint -> `https://xxxx/yyy` + +- Each address must be a complete endpoint + > `https://xxxx/yyy` - Multiple addresses are connected by ', ' ### `DEFAULT_INPUT_TEMPLATE` (optional) diff --git a/README_CN.md b/README_CN.md index 640fe3933..73fbc3f51 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,7 +8,7 @@ 一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。 -[NextChatAI](https://nextchat.dev/chat) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) +[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) [Deploy on Zeabur](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) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) diff --git a/README_JA.md b/README_JA.md index ba3c514dc..416928c26 100644 --- a/README_JA.md +++ b/README_JA.md @@ -5,7 +5,7 @@ ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。 -[NextChatAI](https://nextchat.dev/chat) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N) +[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N) [Zeaburでデプロイ](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) [Zeaburでデプロイ](https://zeabur.com/templates/ZBUEFA) [Gitpodで開く](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) From d2984db6e7deb0fc51ea76abde2b6748067baa28 Mon Sep 17 00:00:00 2001 From: lyf <1910527151@qq.com> Date: Mon, 30 Sep 2024 13:28:14 +0800 Subject: [PATCH 37/54] fix readme --- README.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e10d3bce8..bf7c30594 100644 --- a/README.md +++ b/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 [Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu -[Deploy on Zeabur](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) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) +[Deploy on Zeabur](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) [Deploy on Zeabur](https://zeabur.com/templates/ZBUEFA) [Open in Gitpod](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [](https://monica.im/?utm=nxcrp) @@ -40,7 +40,6 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 ## Enterprise Edition Meeting Your Company's Privatization and Customization Deployment Requirements: - - **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image. - **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members. - **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel. @@ -54,7 +53,6 @@ For enterprise inquiries, please contact: **business@nextchat.dev** ## 企业版 满足企业用户私有化部署和个性化定制需求: - - **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合 - **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用 - **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制 @@ -103,7 +101,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** ## What's New - 🚀 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.9.11 you can use azure endpoint now. - 🚀 v2.8 now we have a client that runs across all platforms! @@ -134,8 +132,8 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) - [x] 插件机制,支持`联网搜索`、`计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) - - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) -- [ ] 本地知识库 + - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353) + - [ ] 本地知识库 ## 最新动态 @@ -335,12 +333,10 @@ 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. 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. > 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. - > Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list. ### `DEFAULT_MODEL` (optional) @@ -350,9 +346,8 @@ Change default model ### `WHITE_WEBDAV_ENDPOINTS` (optional) You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format: - -- Each address must be a complete endpoint - > `https://xxxx/yyy` +- Each address must be a complete endpoint +> `https://xxxx/yyy` - Multiple addresses are connected by ', ' ### `DEFAULT_INPUT_TEMPLATE` (optional) From 35e03e1bcaf228d685e6b2a1ec9168f7892dab98 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 13:44:01 +0800 Subject: [PATCH 38/54] remove code --- src-tauri/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c954deb72..8a11c3b6f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,7 +35,6 @@ tauri = { version = "1.5.4", features = [ "http-all", "window-start-dragging", "window-unmaximize", "window-unminimize", - "linux-protocol-headers", ] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } percent-encoding = "2.3.1" From 3029dcb2f6edbf2fcc805188ac3883a09715fd3f Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 15:32:47 +0800 Subject: [PATCH 39/54] hotfix for run plugin call post api --- app/utils.ts | 5 ++++- src-tauri/src/stream.rs | 18 +++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 9f5dd3150..e542e256d 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -293,7 +293,10 @@ export function fetch( options?: Record, ): Promise { if (window.__TAURI__) { - return tauriStreamFetch(url, options); + return tauriStreamFetch(url, { + ...options, + body: options?.body || options?.data, + }); // const payload = options?.body || options?.data; // return tauriFetch(url, { // ...options, diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 3d21623e6..0fcc02dfc 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -47,10 +47,10 @@ pub async fn stream_fetch( _headers.insert(key.parse::().unwrap(), value.parse().unwrap()); } - // println!("method: {:?}", method); - // println!("url: {:?}", url); - // println!("headers: {:?}", headers); - // println!("headers: {:?}", _headers); + println!("method: {:?}", method); + println!("url: {:?}", url); + println!("headers: {:?}", headers); + println!("headers: {:?}", _headers); let method = method.parse::().map_err(|err| format!("failed to parse method: {}", err))?; let client = Client::builder() @@ -66,12 +66,12 @@ pub async fn stream_fetch( if method == reqwest::Method::POST || method == reqwest::Method::PUT || method == reqwest::Method::PATCH { let body = bytes::Bytes::from(body); - // println!("body: {:?}", body); + println!("body: {:?}", body); request = request.body(body); } - // println!("client: {:?}", client); - // println!("request: {:?}", request); + println!("client: {:?}", client); + println!("request: {:?}", request); let response_future = request.send(); @@ -94,7 +94,7 @@ pub async fn stream_fetch( while let Some(chunk) = stream.next().await { match chunk { Ok(bytes) => { - // println!("chunk: {:?}", bytes); + println!("chunk: {:?}", bytes); if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }) { println!("Failed to emit chunk payload: {:?}", e); } @@ -126,7 +126,7 @@ pub async fn stream_fetch( } } }; - // println!("Response: {:?}", response); + println!("Response: {:?}", response); Ok(response) } From fd3568c459d2339817838b824b6ee5f8d4b59d24 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 15:33:40 +0800 Subject: [PATCH 40/54] hotfix for run plugin call post api --- src-tauri/src/stream.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 0fcc02dfc..3d21623e6 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -47,10 +47,10 @@ pub async fn stream_fetch( _headers.insert(key.parse::().unwrap(), value.parse().unwrap()); } - println!("method: {:?}", method); - println!("url: {:?}", url); - println!("headers: {:?}", headers); - println!("headers: {:?}", _headers); + // println!("method: {:?}", method); + // println!("url: {:?}", url); + // println!("headers: {:?}", headers); + // println!("headers: {:?}", _headers); let method = method.parse::().map_err(|err| format!("failed to parse method: {}", err))?; let client = Client::builder() @@ -66,12 +66,12 @@ pub async fn stream_fetch( if method == reqwest::Method::POST || method == reqwest::Method::PUT || method == reqwest::Method::PATCH { let body = bytes::Bytes::from(body); - println!("body: {:?}", body); + // println!("body: {:?}", body); request = request.body(body); } - println!("client: {:?}", client); - println!("request: {:?}", request); + // println!("client: {:?}", client); + // println!("request: {:?}", request); let response_future = request.send(); @@ -94,7 +94,7 @@ pub async fn stream_fetch( while let Some(chunk) = stream.next().await { match chunk { Ok(bytes) => { - println!("chunk: {:?}", bytes); + // println!("chunk: {:?}", bytes); if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: bytes }) { println!("Failed to emit chunk payload: {:?}", e); } @@ -126,7 +126,7 @@ pub async fn stream_fetch( } } }; - println!("Response: {:?}", response); + // println!("Response: {:?}", response); Ok(response) } From d830c23daba6e80452286aadfec8eeb9cc8652bb Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 15:38:13 +0800 Subject: [PATCH 41/54] hotfix for run plugin call post api --- app/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils.ts b/app/utils.ts index e542e256d..c1476152a 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -295,7 +295,7 @@ export function fetch( if (window.__TAURI__) { return tauriStreamFetch(url, { ...options, - body: options?.body || options?.data, + body: (options?.body || options?.data) as any, }); // const payload = options?.body || options?.data; // return tauriFetch(url, { From 953114041bb8381314c7f880d83c5157d0b6b3ad Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 1 Oct 2024 12:02:29 +0800 Subject: [PATCH 42/54] add connect timeout --- src-tauri/src/stream.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-tauri/src/stream.rs b/src-tauri/src/stream.rs index 3d21623e6..d2c0726b0 100644 --- a/src-tauri/src/stream.rs +++ b/src-tauri/src/stream.rs @@ -1,6 +1,7 @@ // // +use std::time::Duration; use std::error::Error; use std::sync::atomic::{AtomicU32, Ordering}; use std::collections::HashMap; @@ -56,6 +57,7 @@ pub async fn stream_fetch( 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))?; From 9c577ad9d57d47ad5831ca15f15988ba0381ee2c Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 1 Oct 2024 12:55:57 +0800 Subject: [PATCH 43/54] hotfix for plugin runtime --- app/utils.ts | 9 ++++++--- app/utils/chat.ts | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index c1476152a..95880115a 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -320,9 +320,12 @@ export function adapter(config: Record) { const fetchUrl = params ? `${path}?${new URLSearchParams(params as any).toString()}` : path; - return fetch(fetchUrl as string, { ...rest, responseType: "text" }) - .then((res) => res.text()) - .then((data) => ({ data })); + return fetch(fetchUrl as string, { ...rest, responseType: "text" }).then( + (res) => { + const { status, headers } = res; + return res.text().then((data) => ({ status, headers, data })); + }, + ); } export function safeLocalStorage(): { diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 254cef401..3d7960480 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -222,7 +222,10 @@ export function stream( ), ) .then((res) => { - const content = JSON.stringify(res.data); + let content = res.data; + try { + content = JSON.stringify(res.data); + } catch (e) {} if (res.status >= 300) { return Promise.reject(content); } From 919ee51dca25ba03f2d627eaabbe17b578dec45d Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 1 Oct 2024 13:58:50 +0800 Subject: [PATCH 44/54] hover show errorMsg when plugin run error --- app/components/chat.tsx | 1 + app/store/chat.ts | 1 + app/utils.ts | 6 ++++-- app/utils/chat.ts | 11 ++++++----- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 3d519dee7..b45d36f95 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1815,6 +1815,7 @@ function _Chat() { {message?.tools?.map((tool) => (
{tool.isError === false ? ( diff --git a/app/store/chat.ts b/app/store/chat.ts index 968d8cb64..931cad768 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -37,6 +37,7 @@ export type ChatMessageTool = { }; content?: string; isError?: boolean; + errorMsg?: string; }; export type ChatMessage = RequestMessage & { diff --git a/app/utils.ts b/app/utils.ts index 95880115a..83bcea5c0 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -322,8 +322,10 @@ export function adapter(config: Record) { : path; return fetch(fetchUrl as string, { ...rest, responseType: "text" }).then( (res) => { - const { status, headers } = res; - return res.text().then((data) => ({ status, headers, data })); + const { status, headers, statusText } = res; + return res + .text() + .then((data: string) => ({ status, statusText, headers, data })); }, ); } diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 3d7960480..46f232638 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -222,10 +222,7 @@ export function stream( ), ) .then((res) => { - let content = res.data; - try { - content = JSON.stringify(res.data); - } catch (e) {} + let content = res.data || res?.statusText; if (res.status >= 300) { return Promise.reject(content); } @@ -240,7 +237,11 @@ export function stream( return content; }) .catch((e) => { - options?.onAfterTool?.({ ...tool, isError: true }); + options?.onAfterTool?.({ + ...tool, + isError: true, + errorMsg: e.toString(), + }); return e.toString(); }) .then((content) => ({ From d51d31a55959c00279f9d84b302d3ac4de77f559 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 1 Oct 2024 14:40:23 +0800 Subject: [PATCH 45/54] update --- app/utils.ts | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 83bcea5c0..b3d27cbce 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -293,41 +293,23 @@ export function fetch( options?: Record, ): Promise { if (window.__TAURI__) { - return tauriStreamFetch(url, { - ...options, - body: (options?.body || options?.data) as any, - }); - // const payload = options?.body || options?.data; - // 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 tauriStreamFetch(url, options); } return window.fetch(url, options); } export function adapter(config: Record) { - const { baseURL, url, params, ...rest } = config; + const { baseURL, url, params, data: body, ...rest } = config; const path = baseURL ? `${baseURL}${url}` : url; const fetchUrl = params ? `${path}?${new URLSearchParams(params as any).toString()}` : path; - return fetch(fetchUrl as string, { ...rest, responseType: "text" }).then( - (res) => { - const { status, headers, statusText } = res; - return res - .text() - .then((data: string) => ({ status, statusText, headers, data })); - }, - ); + 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(): { From fbb66a4a5d4da9e143c62b482ff27e8b3037d2db Mon Sep 17 00:00:00 2001 From: code-october <148516338+code-october@users.noreply.github.com> Date: Thu, 3 Oct 2024 02:08:10 +0000 Subject: [PATCH 46/54] use safe equal operation --- app/client/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/api.ts b/app/client/api.ts index 48bbde6bc..7a242ea99 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -231,7 +231,7 @@ export function getHeaders(ignoreHeaders: boolean = false) { function getConfig() { const modelConfig = chatStore.currentSession().mask.modelConfig; - const isGoogle = modelConfig.providerName == ServiceProvider.Google; + const isGoogle = modelConfig.providerName === ServiceProvider.Google; const isAzure = modelConfig.providerName === ServiceProvider.Azure; const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic; const isBaidu = modelConfig.providerName == ServiceProvider.Baidu; From cd75461f9e215f6e3140e36359d138dc096abe99 Mon Sep 17 00:00:00 2001 From: little_huang <53588889+little-huang@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:30:25 +0800 Subject: [PATCH 47/54] fix: correct typo in variable name from ALLOWD_PATH to ALLOWED_PATH --- app/api/openai.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/openai.ts b/app/api/openai.ts index 7dfd84e17..bbba69e56 100644 --- a/app/api/openai.ts +++ b/app/api/openai.ts @@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "./auth"; import { requestOpenai } from "./common"; -const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); +const ALLOWED_PATH = new Set(Object.values(OpenaiPath)); function getModels(remoteModelRes: OpenAIListModelResponse) { const config = getServerSideConfig(); @@ -34,7 +34,7 @@ export async function handle( const subpath = params.path.join("/"); - if (!ALLOWD_PATH.has(subpath)) { + if (!ALLOWED_PATH.has(subpath)) { console.log("[OpenAI Route] forbidden path ", subpath); return NextResponse.json( { From 6c1cbe120cb5f018bfc618c7c4382a696a1aa339 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 9 Oct 2024 11:46:49 +0800 Subject: [PATCH 48/54] update --- app/utils/stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/stream.ts b/app/utils/stream.ts index 2a8c13777..313ec4dd5 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -91,7 +91,7 @@ export function fetch(url: string, options?: RequestInit): Promise { headers, }); if (status >= 300) { - setTimeout(close, 50); + setTimeout(close, 100); } return response; }) From a925b424a8b02399d22ed05b3dc28631dbc03bc5 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 9 Oct 2024 13:42:25 +0800 Subject: [PATCH 49/54] fix compressModel, related #5426, fix #5606 #5603 #5575 --- app/store/chat.ts | 50 +++++++++++++++++++++++++++++++++++++++++---- app/store/config.ts | 8 ++++---- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index 931cad768..98163981c 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -16,6 +16,8 @@ import { DEFAULT_SYSTEM_TEMPLATE, KnowledgeCutOffDate, StoreKey, + SUMMARIZE_MODEL, + GEMINI_SUMMARIZE_MODEL, } from "../constant"; import Locale, { getLang } from "../locales"; import { isDalle3, safeLocalStorage } from "../utils"; @@ -23,6 +25,8 @@ import { prettyObject } from "../utils/format"; import { createPersistStore } from "../utils/store"; import { estimateTokenLength } from "../utils/token"; import { ModelConfig, ModelType, useAppConfig } from "./config"; +import { useAccessStore } from "./access"; +import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; const localStorage = safeLocalStorage(); @@ -103,6 +107,29 @@ function createEmptySession(): ChatSession { }; } +function getSummarizeModel(currentModel: string, providerName: 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.providerName]; + } + } + if (currentModel.startsWith("gemini")) { + return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google]; + } + return [currentModel, providerName]; +} + function countMessages(msgs: ChatMessage[]) { return msgs.reduce( (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)), @@ -579,7 +606,13 @@ export const useChatStore = createPersistStore( return; } - const providerName = modelConfig.compressProviderName; + // if not config compressModel, then using getSummarizeModel + const [model, providerName] = modelConfig.compressModel + ? [modelConfig.compressModel, modelConfig.compressProviderName] + : getSummarizeModel( + session.mask.modelConfig.model, + session.mask.modelConfig.providerName, + ); const api: ClientApi = getClientApi(providerName); // remove error messages if any @@ -611,7 +644,7 @@ export const useChatStore = createPersistStore( api.llm.chat({ messages: topicMessages, config: { - model: modelConfig.compressModel, + model, stream: false, providerName, }, @@ -675,7 +708,8 @@ export const useChatStore = createPersistStore( config: { ...modelcfg, stream: true, - model: modelConfig.compressModel, + model, + providerName, }, onUpdate(message) { session.memoryPrompt = message; @@ -728,7 +762,7 @@ export const useChatStore = createPersistStore( }, { name: StoreKey.Chat, - version: 3.2, + version: 3.3, migrate(persistedState, version) { const state = persistedState as any; const newState = JSON.parse( @@ -784,6 +818,14 @@ export const useChatStore = createPersistStore( 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 = undefined; + s.mask.modelConfig.compressProviderName = undefined; + }); + } return newState as any; }, diff --git a/app/store/config.ts b/app/store/config.ts index 3dcd4d86b..c52ee3915 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -71,8 +71,8 @@ export const DEFAULT_CONFIG = { sendMemory: true, historyMessageCount: 4, compressMessageLengthThreshold: 1000, - compressModel: "gpt-4o-mini" as ModelType, - compressProviderName: "OpenAI" as ServiceProvider, + compressModel: undefined, + compressProviderName: undefined, enableInjectSystemPrompts: true, template: config?.template ?? DEFAULT_INPUT_TEMPLATE, size: "1024x1024" as DalleSize, @@ -178,7 +178,7 @@ export const useAppConfig = createPersistStore( }), { name: StoreKey.Config, - version: 4, + version: 4.1, merge(persistedState, currentState) { const state = persistedState as ChatConfig | undefined; @@ -231,7 +231,7 @@ export const useAppConfig = createPersistStore( : config?.template ?? DEFAULT_INPUT_TEMPLATE; } - if (version < 4) { + if (version < 4.1) { state.modelConfig.compressModel = DEFAULT_CONFIG.modelConfig.compressModel; state.modelConfig.compressProviderName = From 93ca303b6c981fd910a7320d8a3d78b335cea1d4 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 9 Oct 2024 13:49:33 +0800 Subject: [PATCH 50/54] fix ts error --- app/store/chat.ts | 17 ++++++++++++----- app/store/config.ts | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/store/chat.ts b/app/store/chat.ts index 98163981c..02adb26b3 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -18,6 +18,7 @@ import { StoreKey, SUMMARIZE_MODEL, GEMINI_SUMMARIZE_MODEL, + ServiceProvider, } from "../constant"; import Locale, { getLang } from "../locales"; import { isDalle3, safeLocalStorage } from "../utils"; @@ -107,7 +108,10 @@ function createEmptySession(): ChatSession { }; } -function getSummarizeModel(currentModel: string, providerName: string) { +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(); @@ -121,7 +125,10 @@ function getSummarizeModel(currentModel: string, providerName: string) { (m) => m.name === SUMMARIZE_MODEL && m.available, ); if (summarizeModel) { - return [summarizeModel.name, summarizeModel.providerName]; + return [ + summarizeModel.name, + summarizeModel.provider?.providerName as string, + ]; } } if (currentModel.startsWith("gemini")) { @@ -613,7 +620,7 @@ export const useChatStore = createPersistStore( session.mask.modelConfig.model, session.mask.modelConfig.providerName, ); - const api: ClientApi = getClientApi(providerName); + const api: ClientApi = getClientApi(providerName as ServiceProvider); // remove error messages if any const messages = session.messages; @@ -822,8 +829,8 @@ export const useChatStore = createPersistStore( if (version < 3.3) { newState.sessions.forEach((s) => { const config = useAppConfig.getState(); - s.mask.modelConfig.compressModel = undefined; - s.mask.modelConfig.compressProviderName = undefined; + s.mask.modelConfig.compressModel = ""; + s.mask.modelConfig.compressProviderName = ""; }); } diff --git a/app/store/config.ts b/app/store/config.ts index c52ee3915..f9ddce4a8 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -71,8 +71,8 @@ export const DEFAULT_CONFIG = { sendMemory: true, historyMessageCount: 4, compressMessageLengthThreshold: 1000, - compressModel: undefined, - compressProviderName: undefined, + compressModel: "", + compressProviderName: "", enableInjectSystemPrompts: true, template: config?.template ?? DEFAULT_INPUT_TEMPLATE, size: "1024x1024" as DalleSize, From c0c8cdbbf37fdde5df0fba4adf6fce477dded75b Mon Sep 17 00:00:00 2001 From: DDMeaqua Date: Wed, 9 Oct 2024 14:36:58 +0800 Subject: [PATCH 51/54] =?UTF-8?q?fix:=20[#5574]=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- README_CN.md | 6 +++--- README_JA.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c72c791b2..4d19bb325 100644 --- a/README.md +++ b/README.md @@ -332,9 +332,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. -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. -> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list. +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. +> 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. > Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list. diff --git a/README_CN.md b/README_CN.md index c5d02477c..fa92f5f07 100644 --- a/README_CN.md +++ b/README_CN.md @@ -216,9 +216,9 @@ ByteDance Api Url. 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 -在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) -> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。 -> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)` +在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) +> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。 +> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)` 在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name) > 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项 diff --git a/README_JA.md b/README_JA.md index 2b0a3ab78..24a28686f 100644 --- a/README_JA.md +++ b/README_JA.md @@ -207,8 +207,8 @@ ByteDance API の URL。 モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。 -Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。 -> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。 +Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。 +> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。 ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。 > 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。 From 4e9bb51d2fe648b429861a20c4c1048fb59ee283 Mon Sep 17 00:00:00 2001 From: ElricLiu <20209191+ElricLiu@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:43:49 +0800 Subject: [PATCH 52/54] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c284e987..b3472291d 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** 企业版咨询: **business@nextchat.dev** - + ## Features From 1dac02e4d63206213913f4abfe934f9c28727fb6 Mon Sep 17 00:00:00 2001 From: Lloyd Zhou Date: Wed, 9 Oct 2024 14:48:43 +0800 Subject: [PATCH 53/54] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3472291d..aba21a510 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev** 企业版咨询: **business@nextchat.dev** - + ## Features From 3e63d405c1b85b62ad7762af64318f92b05ebeb7 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Wed, 9 Oct 2024 16:12:01 +0800 Subject: [PATCH 54/54] update --- app/utils/stream.ts | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/utils/stream.ts b/app/utils/stream.ts index 313ec4dd5..2eda768f3 100644 --- a/app/utils/stream.ts +++ b/app/utils/stream.ts @@ -28,7 +28,8 @@ export function fetch(url: string, options?: RequestInit): Promise { body = [], } = options || {}; let unlisten: Function | undefined; - let request_id = 0; + let setRequestId: Function | undefined; + const requestIdPromise = new Promise((resolve) => (setRequestId = resolve)); const ts = new TransformStream(); const writer = ts.writable.getWriter(); @@ -47,20 +48,22 @@ export function fetch(url: string, options?: RequestInit): Promise { } // @ts-ignore 2. listen response multi times, and write to Response.body window.__TAURI__.event - .listen("stream-response", (e: ResponseEvent) => { - 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(); - } - }) + .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 = { @@ -83,8 +86,8 @@ export function fetch(url: string, options?: RequestInit): Promise { : [], }) .then((res: StreamResponse) => { - request_id = res.request_id; - const { status, status_text: statusText, headers } = res; + const { request_id, status, status_text: statusText, headers } = res; + setRequestId?.(request_id); const response = new Response(ts.readable, { status, statusText,