diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts index 8d84adbde3d..a24c02393d2 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 00000000000..21f1cb1a8aa --- /dev/null +++ b/src-tauri/src/fetch.rs @@ -0,0 +1,169 @@ +// +// HTTP request handler module +// + +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, +} + +#[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)); + } + } + } + + // 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)) + .connect_timeout(Duration::new(10, 0)) + .timeout(Duration::new(30, 0)) + .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))? + ); + + // For request methods that need a body, add the request 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); + } + } + + // Send request + 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) + })?; + + // Get response status and headers + 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() + ); + } + + // Read response body + 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 { + + // Convert string body to bytes + let body_bytes = body.into_bytes(); + + // 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))?; + + Ok(response_text) +} + +#[tauri::command] +pub async fn http_fetch_json( + method: String, + url: String, + headers: HashMap, + body: serde_json::Value, +) -> Result { + + // 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(); + + // 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()); + } + + // Call the main fetch method + let response = http_fetch(method, url, json_headers, body_bytes).await?; + + // 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))?; + + Ok(response_json) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d04969c043b..0d07f4941b1 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");