From 4d6b981a54d676a4d70b767418ec842bb4f3114a Mon Sep 17 00:00:00 2001 From: butterfly Date: Tue, 26 Mar 2024 11:43:55 +0800 Subject: [PATCH 01/37] 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 7c70fe1a5ac..c6290d8e015 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 d84d51b475a1a6ad18efed46121af9adbc7b98f3 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sat, 28 Sep 2024 01:19:39 +0800 Subject: [PATCH 02/37] 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 9a8bebf38c7..5be7bb2d9f7 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 47d12e1190b..fcc06d163cd 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 387584491ba..31ecfd83e4d 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 ed3ec32f37b..792c656cf51 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 03/37] 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 8ee636bcd3c..a1453dc33b4 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 5be7bb2d9f7..fbe77c114c5 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 00000000000..8f9ccfbaa1d --- /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 fcc06d163cd..c9baffc0acc 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 31ecfd83e4d..c954deb72a8 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 792c656cf51..e382082572f 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 00000000000..5e84e0f00d1 --- /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 3898c507c466c13537330fd6feb7b960e330c774 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sun, 29 Sep 2024 19:44:09 +0800 Subject: [PATCH 04/37] using stream_fetch in App --- app/utils.ts | 8 ++- app/utils/chat.ts | 2 + app/utils/stream.ts | 152 +++++++++++++++++++--------------------- src-tauri/src/main.rs | 4 +- src-tauri/src/stream.rs | 127 +++++++++++++++++++-------------- 5 files changed, 156 insertions(+), 137 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index fbe77c114c5..baf45abe573 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 7f3bb23c58e..359b2c53ebc 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 8f9ccfbaa1d..09b898431e7 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); + writer.ready.then(() => { + try { + writer.releaseLock(); + } catch (e) { + console.error(e); + } + ts.writable.close(); + }); + }; + + const response = new Response(ts.readable, { + status, + statusText, + headers, }); - } - // @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; + 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(); } - 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]), - ); + }) + .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 e382082572f..d04969c043b 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 5e84e0f00d1..514e6229803 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; - - 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 _headers = HeaderMap::new(); + 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))?, + 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(); + window.emit(event_name, EndPayload { request_id, status: 0 }).unwrap(); + }); + + StreamResponse { + request_id, + status, + status_text: "OK".to_string(), + headers, } - 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(); + } + 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(), } } - }); - return ResponseBuilder::new() - .header("Access-Control-Allow-Origin", "*") - .status(200).body(request_id.to_string().into()) + }; + Ok(response) } + From 9e6ee50fa6335fe9c405b69e09bb7b6046aff42c Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Sun, 29 Sep 2024 20:32:36 +0800 Subject: [PATCH 05/37] 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 09b898431e7..f8c272e4296 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 514e6229803..81710c73346 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 06/37] 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 f8c272e4296..dd665e71c75 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 81710c73346..97989ba7e4f 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 b5f6e5a5989a36ecb1b5f2b37209f2d4bffc607b Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 00:38:30 +0800 Subject: [PATCH 07/37] 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 40abdc8d9f4..b3d9f6d8ce6 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 dd665e71c75..625ecea1f22 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 97989ba7e4f..a35e2a00123 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 08/37] 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 31569516389..6b2f65952c7 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 09/37] 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 4ade9ebb98f..727e3aebf14 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 7826838a61e..1a83bd53aa1 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 c360417c602..4f3294d5e02 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 a6e2d426ee3..279be815f3e 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 3c2607271bf..7a74dd4f3b6 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 3931672e661..07bfeda5807 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 6b197974571..4570dca9722 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 0a8d6203ae5..0b2d91c995d 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 3e8f1a45957..28f5b56f243 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 a54a973daa6..a06b8f05062 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 9fcd227e7c0..74050733c79 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 9db60d5f410..c53a7a82a3d 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 f5e5ce6f0a2..00000000000 --- 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 10/37] 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 727e3aebf14..86229a14705 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 4f3294d5e02..2511a696b9b 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 279be815f3e..000a9e278db 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 7a74dd4f3b6..dcf300a0f1b 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 07bfeda5807..de638829ee5 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 28f5b56f243..3610fac0a48 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 c53a7a82a3d..8477c1e4ba7 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 359b2c53ebc..254cef401b1 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 625ecea1f22..e8850fcba8d 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 11/37] 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 a35e2a00123..9aae3d164ea 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 12/37] 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 de638829ee5..55a39d0ccca 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 4570dca9722..e0ef3494fe7 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 0b2d91c995d..a2263361143 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 13/37] 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 a1453dc33b4..8ee636bcd3c 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 9aae3d164ea..f7d5a7693d7 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 14/37] 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 6b2f65952c7..4887a1021ad 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 15/37] 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 f7d5a7693d7..51d844305e6 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 16/37] 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 4887a1021ad..9f5dd315090 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 e8850fcba8d..2a8c13777e5 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 17/37] 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 74050733c79..dec3a725886 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 18/37] 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 51d844305e6..938c663e1f9 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 19/37] 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 938c663e1f9..3d21623e6f3 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 20/37] 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 be5e91d65c7..5887369ff40 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 eb0d411cba6..cc137ee8afd 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 35e03e1bcaf228d685e6b2a1ec9168f7892dab98 Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Mon, 30 Sep 2024 13:44:01 +0800 Subject: [PATCH 21/37] 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 c954deb72a8..8a11c3b6f98 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 22/37] 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 9f5dd315090..e542e256d57 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 3d21623e6f3..0fcc02dfc33 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 23/37] 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 0fcc02dfc33..3d21623e6f3 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 24/37] 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 e542e256d57..c1476152a53 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 25/37] 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 3d21623e6f3..d2c0726b0b4 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 26/37] 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 c1476152a53..95880115a3d 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 254cef401b1..3d796048000 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 27/37] 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 3d519dee722..b45d36f9587 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 968d8cb6422..931cad76883 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 95880115a3d..83bcea5c0ec 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 3d796048000..46f23263895 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 28/37] update --- app/utils.ts | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/app/utils.ts b/app/utils.ts index 83bcea5c0ec..b3d27cbce2c 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 29/37] 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 48bbde6bc81..7a242ea99dd 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 30/37] 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 7dfd84e1785..bbba69e569c 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 31/37] 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 2a8c13777e5..313ec4dd5b8 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 32/37] 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 931cad76883..98163981cd3 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 3dcd4d86b80..c52ee3915b9 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 33/37] 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 98163981cd3..02adb26b3b9 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 c52ee3915b9..f9ddce4a80c 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 34/37] =?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 c72c791b264..4d19bb3252d 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 c5d02477c2c..fa92f5f0776 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 2b0a3ab782c..24a28686f34 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 35/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c284e987bf..b3472291d39 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 36/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3472291d39..aba21a5108b 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 37/37] 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 313ec4dd5b8..2eda768f36f 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,