From 8998f79006fa301773d304f2b1366b2667f78bf5 Mon Sep 17 00:00:00 2001 From: EvanWu <850123119@qq.com> Date: Tue, 9 Sep 2025 15:05:38 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BA=91=E7=AB=AF=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20upstash=E6=94=AF=E6=8C=81tauri=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/cloud/upstash.ts | 67 +++++++++++++- src-tauri/src/fetch.rs | 177 +++++++++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 8 +- 3 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 src-tauri/src/fetch.rs diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts index 8d84adbde..a24c02393 100644 --- a/app/utils/cloud/upstash.ts +++ b/app/utils/cloud/upstash.ts @@ -5,6 +5,62 @@ import { chunks } from "../format"; export type UpstashConfig = SyncStore["upstash"]; export type UpStashClient = ReturnType; +async function httpFetch( + url: string, + options?: RequestInit, +): Promise { + if (window.__TAURI__) { + // 转换 RequestInit 格式为 Tauri 期望的格式 + const method = options?.method || "GET"; + const headers: Record = {}; + + // 处理 headers + if (options?.headers) { + if (options.headers instanceof Headers) { + options.headers.forEach((value, key) => { + headers[key] = value; + }); + } else if (Array.isArray(options.headers)) { + options.headers.forEach(([key, value]) => { + headers[key] = value; + }); + } else { + Object.assign(headers, options.headers); + } + } + + // 处理 body + let body: number[] = []; + if (options?.body) { + if (typeof options.body === "string") { + body = Array.from(new TextEncoder().encode(options.body)); + } else if (options.body instanceof ArrayBuffer) { + body = Array.from(new Uint8Array(options.body)); + } else if (options.body instanceof Uint8Array) { + body = Array.from(options.body); + } else { + // 其他类型转换为字符串 + body = Array.from(new TextEncoder().encode(String(options.body))); + } + } + + const response = await window.__TAURI__.invoke("http_fetch", { + method, + url, + headers, + body, + }); + + // 将 Tauri 响应转换为 Response 对象格式 + return new Response(new Uint8Array(response.body), { + status: response.status, + statusText: response.status_text, + headers: new Headers(response.headers), + }); + } + return fetch(url, options); +} + export function createUpstashClient(store: SyncStore) { const config = store.upstash; const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username; @@ -17,7 +73,7 @@ export function createUpstashClient(store: SyncStore) { return { async check() { try { - const res = await fetch(this.path(`get/${storeKey}`, proxyUrl), { + const res = await httpFetch(this.path(`get/${storeKey}`, proxyUrl), { method: "GET", headers: this.headers(), }); @@ -30,7 +86,7 @@ export function createUpstashClient(store: SyncStore) { }, async redisGet(key: string) { - const res = await fetch(this.path(`get/${key}`, proxyUrl), { + const res = await httpFetch(this.path(`get/${key}`, proxyUrl), { method: "GET", headers: this.headers(), }); @@ -42,7 +98,7 @@ export function createUpstashClient(store: SyncStore) { }, async redisSet(key: string, value: string) { - const res = await fetch(this.path(`set/${key}`, proxyUrl), { + const res = await httpFetch(this.path(`set/${key}`, proxyUrl), { method: "POST", headers: this.headers(), body: value, @@ -81,6 +137,9 @@ export function createUpstashClient(store: SyncStore) { }; }, path(path: string, proxyUrl: string = "") { + if (window.__TAURI__) { + return config.endpoint + "/" + path; + } if (!path.endsWith("/")) { path += "/"; } @@ -96,7 +155,7 @@ export function createUpstashClient(store: SyncStore) { const pathPrefix = "/api/upstash/"; try { - let u = new URL(proxyUrl + pathPrefix + path); + let u = new URL(proxyUrl + pathPrefix + path, window.location.origin); // add query params u.searchParams.append("endpoint", config.endpoint); url = u.toString(); diff --git a/src-tauri/src/fetch.rs b/src-tauri/src/fetch.rs new file mode 100644 index 000000000..36048eec2 --- /dev/null +++ b/src-tauri/src/fetch.rs @@ -0,0 +1,177 @@ +// +// 普通 HTTP 请求处理模块 +// + +use std::time::Duration; +use std::error::Error; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::collections::HashMap; +use reqwest::Client; +use reqwest::header::{HeaderName, HeaderMap}; + +static REQUEST_COUNTER: AtomicU32 = AtomicU32::new(0); + +#[derive(Debug, Clone, serde::Serialize)] +pub struct FetchResponse { + request_id: u32, + status: u16, + status_text: String, + headers: HashMap, + body: Vec, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ErrorResponse { + request_id: u32, + status: u16, + status_text: String, + error: String, +} + +#[tauri::command] +pub async fn http_fetch( + method: String, + url: String, + headers: HashMap, + body: Vec, +) -> Result { + + let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::SeqCst); + + let mut _headers = HeaderMap::new(); + for (key, value) in &headers { + match key.parse::() { + Ok(header_name) => { + match value.parse() { + Ok(header_value) => { + _headers.insert(header_name, header_value); + } + Err(err) => { + return Err(format!("failed to parse header value '{}': {}", value, err)); + } + } + } + Err(err) => { + return Err(format!("failed to parse header name '{}': {}", key, err)); + } + } + } + + // 解析 HTTP 方法 + let method = method.parse::() + .map_err(|err| format!("failed to parse method: {}", err))?; + + // 创建客户端 + let client = Client::builder() + .default_headers(_headers) + .redirect(reqwest::redirect::Policy::limited(3)) + .connect_timeout(Duration::new(10, 0)) + .timeout(Duration::new(30, 0)) + .build() + .map_err(|err| format!("failed to create client: {}", err))?; + + // 构建请求 + let mut request = client.request( + method.clone(), + url.parse::() + .map_err(|err| format!("failed to parse url: {}", err))? + ); + + // 对于需要 body 的请求方法,添加请求体 + if method == reqwest::Method::POST + || method == reqwest::Method::PUT + || method == reqwest::Method::PATCH + || method == reqwest::Method::DELETE { + if !body.is_empty() { + let body_bytes = bytes::Bytes::from(body); + request = request.body(body_bytes); + } + } + + // 发送请求 + let response = request.send().await + .map_err(|err| { + let error_msg = err.source() + .map(|e| e.to_string()) + .unwrap_or_else(|| err.to_string()); + format!("request failed: {}", error_msg) + })?; + + // 获取响应状态和头部 + let status = response.status().as_u16(); + let status_text = response.status().canonical_reason() + .unwrap_or("Unknown") + .to_string(); + + let mut response_headers = HashMap::new(); + for (name, value) in response.headers() { + response_headers.insert( + name.as_str().to_string(), + std::str::from_utf8(value.as_bytes()) + .unwrap_or("") + .to_string() + ); + } + + // 读取响应体 + let response_body = response.bytes().await + .map_err(|err| format!("failed to read response body: {}", err))?; + + Ok(FetchResponse { + request_id, + status, + status_text, + headers: response_headers, + body: response_body.to_vec(), + }) +} + +#[tauri::command] +pub async fn http_fetch_text( + method: String, + url: String, + headers: HashMap, + body: String, +) -> Result { + + // 将字符串 body 转换为字节 + let body_bytes = body.into_bytes(); + + // 调用主要的 fetch 方法 + let response = http_fetch(method, url, headers, body_bytes).await?; + + // 将响应体转换为字符串 + let response_text = String::from_utf8(response.body) + .map_err(|err| format!("failed to convert response to text: {}", err))?; + + Ok(response_text) +} + +#[tauri::command] +pub async fn http_fetch_json( + method: String, + url: String, + headers: HashMap, + body: serde_json::Value, +) -> Result { + + // 将 JSON 转换为字符串再转换为字节 + let body_string = serde_json::to_string(&body) + .map_err(|err| format!("failed to serialize JSON body: {}", err))?; + let body_bytes = body_string.into_bytes(); + + // 确保设置了正确的 Content-Type + let mut json_headers = headers; + if !json_headers.contains_key("content-type") && !json_headers.contains_key("Content-Type") { + json_headers.insert("Content-Type".to_string(), "application/json".to_string()); + } + + // 调用主要的 fetch 方法 + let response = http_fetch(method, url, json_headers, body_bytes).await?; + + // 将响应体解析为 JSON + let response_json: serde_json::Value = serde_json::from_slice(&response.body) + .map_err(|err| format!("failed to parse response as JSON: {}", err))?; + + Ok(response_json) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d04969c04..0d07f4941 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,10 +2,16 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod stream; +mod fetch; fn main() { tauri::Builder::default() - .invoke_handler(tauri::generate_handler![stream::stream_fetch]) + .invoke_handler(tauri::generate_handler![ + stream::stream_fetch, + fetch::http_fetch, + fetch::http_fetch_text, + fetch::http_fetch_json + ]) .plugin(tauri_plugin_window_state::Builder::default().build()) .run(tauri::generate_context!()) .expect("error while running tauri application"); From 3fd852a5e6967e7af53d2c38e35d6f4c99a873b6 Mon Sep 17 00:00:00 2001 From: EvanWu <850123119@qq.com> Date: Tue, 9 Sep 2025 15:45:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=20fetch.rs=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=AD=E7=9A=84=E6=B3=A8=E9=87=8A=EF=BC=8C?= =?UTF-8?q?=E5=B0=86=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E4=B8=BA=E8=8B=B1=E6=96=87=EF=BC=8C=E4=BB=A5=E6=8F=90=E9=AB=98?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/fetch.rs | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src-tauri/src/fetch.rs b/src-tauri/src/fetch.rs index 36048eec2..21f1cb1a8 100644 --- a/src-tauri/src/fetch.rs +++ b/src-tauri/src/fetch.rs @@ -1,5 +1,5 @@ // -// 普通 HTTP 请求处理模块 +// HTTP request handler module // use std::time::Duration; @@ -20,14 +20,6 @@ pub struct FetchResponse { body: Vec, } -#[derive(Debug, Clone, serde::Serialize)] -pub struct ErrorResponse { - request_id: u32, - status: u16, - status_text: String, - error: String, -} - #[tauri::command] pub async fn http_fetch( method: String, @@ -57,11 +49,11 @@ pub async fn http_fetch( } } - // 解析 HTTP 方法 + // Parse HTTP method let method = method.parse::() .map_err(|err| format!("failed to parse method: {}", err))?; - // 创建客户端 + // Create client let client = Client::builder() .default_headers(_headers) .redirect(reqwest::redirect::Policy::limited(3)) @@ -70,14 +62,14 @@ pub async fn http_fetch( .build() .map_err(|err| format!("failed to create client: {}", err))?; - // 构建请求 + // Build request let mut request = client.request( method.clone(), url.parse::() .map_err(|err| format!("failed to parse url: {}", err))? ); - // 对于需要 body 的请求方法,添加请求体 + // For request methods that need a body, add the request body if method == reqwest::Method::POST || method == reqwest::Method::PUT || method == reqwest::Method::PATCH @@ -88,7 +80,7 @@ pub async fn http_fetch( } } - // 发送请求 + // Send request let response = request.send().await .map_err(|err| { let error_msg = err.source() @@ -97,7 +89,7 @@ pub async fn http_fetch( format!("request failed: {}", error_msg) })?; - // 获取响应状态和头部 + // Get response status and headers let status = response.status().as_u16(); let status_text = response.status().canonical_reason() .unwrap_or("Unknown") @@ -113,7 +105,7 @@ pub async fn http_fetch( ); } - // 读取响应体 + // Read response body let response_body = response.bytes().await .map_err(|err| format!("failed to read response body: {}", err))?; @@ -134,13 +126,13 @@ pub async fn http_fetch_text( body: String, ) -> Result { - // 将字符串 body 转换为字节 + // Convert string body to bytes let body_bytes = body.into_bytes(); - // 调用主要的 fetch 方法 + // Call the main fetch method let response = http_fetch(method, url, headers, body_bytes).await?; - // 将响应体转换为字符串 + // Convert response body to string let response_text = String::from_utf8(response.body) .map_err(|err| format!("failed to convert response to text: {}", err))?; @@ -155,21 +147,21 @@ pub async fn http_fetch_json( body: serde_json::Value, ) -> Result { - // 将 JSON 转换为字符串再转换为字节 + // Convert JSON to string and then to bytes let body_string = serde_json::to_string(&body) .map_err(|err| format!("failed to serialize JSON body: {}", err))?; let body_bytes = body_string.into_bytes(); - // 确保设置了正确的 Content-Type + // Ensure the correct Content-Type is set let mut json_headers = headers; if !json_headers.contains_key("content-type") && !json_headers.contains_key("Content-Type") { json_headers.insert("Content-Type".to_string(), "application/json".to_string()); } - // 调用主要的 fetch 方法 + // Call the main fetch method let response = http_fetch(method, url, json_headers, body_bytes).await?; - // 将响应体解析为 JSON + // Parse response body as JSON let response_json: serde_json::Value = serde_json::from_slice(&response.body) .map_err(|err| format!("failed to parse response as JSON: {}", err))?;