Parcourir la source

公用的http功能

黄中银 il y a 2 semaines
Parent
commit
5575291f96

+ 1 - 1
src-tauri/Cargo.toml

@@ -27,7 +27,7 @@ tauri-plugin-os = "2"
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
 tokio = { version = "1", features = ["full"] }
-reqwest = { version = "0.12", features = ["json", "stream"] }
+reqwest = { version = "0.12", features = ["json", "stream", "gzip"] }
 futures = "0.3"
 log = "0.4"
 chrono = { version = "0.4", features = ["serde"] }

+ 31 - 32
src-tauri/src/commands/install.rs

@@ -1,5 +1,6 @@
 use crate::commands::AppState;
 use crate::commands::config::{get_git_mirror_config, get_nodejs_mirror_config};
+use crate::utils::http::{get_client, HttpRequest};
 use crate::utils::shell::{run_program, run_shell_hidden, CommandOptions};
 use futures::StreamExt;
 use serde::{Deserialize, Serialize};
@@ -34,14 +35,17 @@ where
         0
     };
 
-    let client = reqwest::Client::new();
+    // 使用全局 HTTP 客户端
+    let client = get_client();
+
+    log::info!("[HTTP] GET {} (下载文件)", url);
 
     // 构建请求,支持断点续传
     let mut request = client.get(url);
     if downloaded_size > 0 {
         request = request.header("Range", format!("bytes={}-", downloaded_size));
         log::info!(
-            "断点续传: 从 {:.1}MB 处继续下载",
+            "[HTTP] 断点续传: 从 {:.1}MB 处继续下载",
             downloaded_size as f64 / 1024.0 / 1024.0
         );
     }
@@ -49,7 +53,10 @@ where
     let response = request
         .send()
         .await
-        .map_err(|e| format!("Failed to send request: {}", e))?;
+        .map_err(|e| {
+            log::error!("[HTTP] 下载请求失败: {} - {}", url, e);
+            format!("Failed to send request: {}", e)
+        })?;
 
     let status = response.status();
 
@@ -543,17 +550,14 @@ where
             "software": "VS Code"
         })));
 
-        let client = reqwest::Client::new();
-
         // 从官方 API 获取下载地址
-        let update_response = client
-            .get("https://update.code.visualstudio.com/api/update/win32-x64/stable/latest")
-            .header("User-Agent", "Claude-AI-Installer")
+        let update_info: serde_json::Value = HttpRequest::get("https://update.code.visualstudio.com/api/update/win32-x64/stable/latest")
+            .timeout_secs(30)
             .send()
             .await
-            .map_err(|e| format!("Failed to get VS Code version info: {}", e))?;
-
-        let update_info: serde_json::Value = update_response.json().await.map_err(|e| e.to_string())?;
+            .map_err(|e| format!("Failed to get VS Code version info: {}", e))?
+            .json()
+            .await?;
 
         let download_url = update_info.get("url")
             .and_then(|v| v.as_str())
@@ -641,17 +645,14 @@ where
             "software": "VS Code"
         })));
 
-        let client = reqwest::Client::new();
-
         // 从官方 API 获取下载地址
-        let update_response = client
-            .get("https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest")
-            .header("User-Agent", "Claude-AI-Installer")
+        let update_info: serde_json::Value = HttpRequest::get("https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest")
+            .timeout_secs(30)
             .send()
             .await
-            .map_err(|e| format!("Failed to get VS Code version info: {}", e))?;
-
-        let update_info: serde_json::Value = update_response.json().await.map_err(|e| e.to_string())?;
+            .map_err(|e| format!("Failed to get VS Code version info: {}", e))?
+            .json()
+            .await?;
 
         let download_url = update_info.get("url")
             .and_then(|v| v.as_str())
@@ -740,18 +741,16 @@ where
         let mirror = mirror_config.mirror.as_str();
 
         let download_url: String;
-        let client = reqwest::Client::new();
 
         if mirror == "huaweicloud" {
             // 华为云镜像:从目录页面解析最新版本
-            let versions_response = client
-                .get("https://mirrors.huaweicloud.com/git-for-windows/")
-                .header("User-Agent", "Claude-AI-Installer")
+            let html = HttpRequest::get("https://mirrors.huaweicloud.com/git-for-windows/")
+                .timeout_secs(30)
                 .send()
                 .await
-                .map_err(|e| e.to_string())?;
-
-            let html = versions_response.text().await.map_err(|e| e.to_string())?;
+                .map_err(|e| e.to_string())?
+                .text()
+                .await?;
 
             // 解析版本号,格式如 v2.47.1.windows.1/
             let version_regex = regex::Regex::new(r#"href="v(\d+\.\d+\.\d+)\.windows\.\d+/""#)
@@ -775,14 +774,14 @@ where
             );
         } else {
             // GitHub 官方:从 API 获取最新版本
-            let releases_response = client
-                .get("https://api.github.com/repos/git-for-windows/git/releases/latest")
-                .header("User-Agent", "Claude-AI-Installer")
+            let release: serde_json::Value = HttpRequest::get("https://api.github.com/repos/git-for-windows/git/releases/latest")
+                .header("Accept", "application/vnd.github.v3+json")
+                .timeout_secs(30)
                 .send()
                 .await
-                .map_err(|e| e.to_string())?;
-
-            let release: serde_json::Value = releases_response.json().await.map_err(|e| e.to_string())?;
+                .map_err(|e| e.to_string())?
+                .json()
+                .await?;
 
             let assets = release.get("assets").and_then(|a| a.as_array()).ok_or("No assets found")?;
 

+ 28 - 55
src-tauri/src/commands/software.rs

@@ -1,4 +1,5 @@
 use crate::utils::shell::run_shell_hidden;
+use crate::utils::http::HttpRequest;
 use serde::{Deserialize, Serialize};
 use regex::Regex;
 use super::config::{get_git_mirror_config, get_nodejs_mirror_config};
@@ -142,8 +143,6 @@ async fn get_nodejs_versions() -> Result<VersionResult, String> {
     let mirror_config = get_nodejs_mirror_config().await;
     let mirror = mirror_config.mirror.as_str();
 
-    let client = reqwest::Client::new();
-
     // 根据镜像源选择 API 地址
     let (api_url, mirror_name) = if mirror == "huaweicloud" {
         ("https://mirrors.huaweicloud.com/nodejs/index.json", "华为云镜像")
@@ -151,14 +150,13 @@ async fn get_nodejs_versions() -> Result<VersionResult, String> {
         ("https://nodejs.org/dist/index.json", "Node.js 官方")
     };
 
-    let response = client
-        .get(api_url)
-        .timeout(std::time::Duration::from_secs(10))
+    let versions: Vec<serde_json::Value> = HttpRequest::get(api_url)
+        .timeout_secs(15)
         .send()
         .await
-        .map_err(|e| format!("请求 {} 失败: {}", mirror_name, e))?;
-
-    let versions: Vec<serde_json::Value> = response.json().await.map_err(|e| e.to_string())?;
+        .map_err(|e| format!("请求 {} 失败: {}", mirror_name, e))?
+        .json()
+        .await?;
 
     let version_items: Vec<VersionItem> = versions
         .iter()
@@ -200,17 +198,15 @@ async fn get_git_versions() -> Result<VersionResult, String> {
     let mirror_config = get_git_mirror_config().await;
     let mirror = mirror_config.mirror.as_str();
 
-    let client = reqwest::Client::new();
-
     // 根据镜像类型选择不同的获取方式
     let (versions, mirror_name) = match mirror {
         "huaweicloud" => {
-            let versions = get_git_versions_from_huaweicloud(&client).await;
+            let versions = get_git_versions_from_huaweicloud().await;
             (versions, "华为云镜像")
         }
         _ => {
             // 默认使用 GitHub
-            let versions = get_git_versions_from_github(&client).await;
+            let versions = get_git_versions_from_github().await;
             (versions, "GitHub 官方")
         }
     };
@@ -240,33 +236,26 @@ async fn get_git_versions() -> Result<VersionResult, String> {
 }
 
 /// 从 GitHub API 获取 Git 版本列表
-async fn get_git_versions_from_github(client: &reqwest::Client) -> Vec<String> {
-    let response = match client
-        .get("https://api.github.com/repos/git-for-windows/git/releases")
-        .header("User-Agent", "Claude-AI-Installer")
+async fn get_git_versions_from_github() -> Vec<String> {
+    // 只获取前30个releases,减少响应体大小
+    let response = match HttpRequest::get("https://api.github.com/repos/git-for-windows/git/releases?per_page=30")
         .header("Accept", "application/vnd.github.v3+json")
-        .timeout(std::time::Duration::from_secs(10))
+        .timeout_secs(30)
         .send()
         .await
     {
         Ok(resp) => resp,
-        Err(e) => {
-            eprintln!("GitHub API 请求失败: {}", e);
-            return vec![];
-        }
+        Err(_) => return vec![],
     };
 
-    if !response.status().is_success() {
-        eprintln!("GitHub API 返回错误状态: {}", response.status());
+    if !response.is_success() {
+        log::error!("GitHub API 返回错误状态: {}", response.status());
         return vec![];
     }
 
     let releases: Vec<serde_json::Value> = match response.json().await {
         Ok(r) => r,
-        Err(e) => {
-            eprintln!("解析 GitHub 响应失败: {}", e);
-            return vec![];
-        }
+        Err(_) => return vec![],
     };
 
     let mut versions: Vec<String> = vec![];
@@ -292,32 +281,24 @@ async fn get_git_versions_from_github(client: &reqwest::Client) -> Vec<String> {
 }
 
 /// 从华为云镜像获取 Git 版本列表
-async fn get_git_versions_from_huaweicloud(client: &reqwest::Client) -> Vec<String> {
-    let response = match client
-        .get("https://mirrors.huaweicloud.com/git-for-windows/")
-        .header("User-Agent", "Claude-AI-Installer")
-        .timeout(std::time::Duration::from_secs(10))
+async fn get_git_versions_from_huaweicloud() -> Vec<String> {
+    let response = match HttpRequest::get("https://mirrors.huaweicloud.com/git-for-windows/")
+        .timeout_secs(15)
         .send()
         .await
     {
         Ok(resp) => resp,
-        Err(e) => {
-            eprintln!("华为云镜像请求失败: {}", e);
-            return vec![];
-        }
+        Err(_) => return vec![],
     };
 
-    if !response.status().is_success() {
-        eprintln!("华为云镜像返回错误状态: {}", response.status());
+    if !response.is_success() {
+        log::error!("华为云镜像返回错误状态: {}", response.status());
         return vec![];
     }
 
     let html = match response.text().await {
         Ok(h) => h,
-        Err(e) => {
-            eprintln!("读取华为云响应失败: {}", e);
-            return vec![];
-        }
+        Err(_) => return vec![],
     };
 
     // 解析 HTML 页面,查找版本目录链接
@@ -355,26 +336,18 @@ async fn get_git_versions_from_huaweicloud(client: &reqwest::Client) -> Vec<Stri
 }
 
 async fn get_vscode_versions() -> Result<VersionResult, String> {
-    let client = reqwest::Client::new();
-
     // 从 VS Code 官方 API 获取版本列表
-    let response = client
-        .get("https://update.code.visualstudio.com/api/releases/stable")
-        .header("User-Agent", "Claude-AI-Installer")
-        .timeout(std::time::Duration::from_secs(10))
+    let response = HttpRequest::get("https://update.code.visualstudio.com/api/releases/stable")
+        .timeout_secs(15)
         .send()
-        .await
-        .map_err(|e| format!("请求 VS Code 版本列表失败: {}", e))?;
+        .await?;
 
-    if !response.status().is_success() {
+    if !response.is_success() {
         return Err(format!("VS Code API 返回错误状态: {}", response.status()));
     }
 
     // API 返回格式是版本号数组: ["1.96.0", "1.95.3", ...]
-    let versions: Vec<String> = response
-        .json()
-        .await
-        .map_err(|e| format!("解析 VS Code 版本列表失败: {}", e))?;
+    let versions: Vec<String> = response.json().await?;
 
     let version_items: Vec<VersionItem> = versions
         .into_iter()

+ 228 - 0
src-tauri/src/utils/http.rs

@@ -0,0 +1,228 @@
+//! 统一的 HTTP 客户端模块
+//!
+//! 提供统一的 HTTP 请求功能,支持:
+//! - 自动 gzip 解压
+//! - 请求/响应日志记录
+//! - 统一的超时配置
+//! - 统一的 User-Agent
+
+use reqwest::{Client, Response};
+use std::sync::OnceLock;
+use std::time::Duration;
+
+/// 默认超时时间(秒)
+const DEFAULT_TIMEOUT_SECS: u64 = 30;
+
+/// 默认 User-Agent
+const USER_AGENT: &str = "Claude-AI-Installer/1.0";
+
+/// 全局 HTTP 客户端(单例)
+static HTTP_CLIENT: OnceLock<Client> = OnceLock::new();
+
+/// 获取全局 HTTP 客户端
+pub fn get_client() -> &'static Client {
+    HTTP_CLIENT.get_or_init(|| {
+        Client::builder()
+            .gzip(true)
+            .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
+            .user_agent(USER_AGENT)
+            .build()
+            .unwrap_or_else(|e| {
+                log::error!("创建 HTTP 客户端失败: {}, 使用默认客户端", e);
+                Client::new()
+            })
+    })
+}
+
+/// HTTP 请求构建器
+pub struct HttpRequest {
+    url: String,
+    method: HttpMethod,
+    headers: Vec<(String, String)>,
+    timeout: Option<Duration>,
+}
+
+#[derive(Clone, Copy)]
+pub enum HttpMethod {
+    Get,
+    Post,
+}
+
+impl HttpRequest {
+    /// 创建 GET 请求
+    pub fn get(url: &str) -> Self {
+        Self {
+            url: url.to_string(),
+            method: HttpMethod::Get,
+            headers: vec![],
+            timeout: None,
+        }
+    }
+
+    /// 创建 POST 请求
+    #[allow(dead_code)]
+    pub fn post(url: &str) -> Self {
+        Self {
+            url: url.to_string(),
+            method: HttpMethod::Post,
+            headers: vec![],
+            timeout: None,
+        }
+    }
+
+    /// 添加请求头
+    pub fn header(mut self, key: &str, value: &str) -> Self {
+        self.headers.push((key.to_string(), value.to_string()));
+        self
+    }
+
+    /// 设置超时时间
+    pub fn timeout(mut self, duration: Duration) -> Self {
+        self.timeout = Some(duration);
+        self
+    }
+
+    /// 设置超时时间(秒)
+    pub fn timeout_secs(self, secs: u64) -> Self {
+        self.timeout(Duration::from_secs(secs))
+    }
+
+    /// 发送请求并返回响应
+    pub async fn send(self) -> Result<HttpResponse, String> {
+        let client = get_client();
+        let method_str = match self.method {
+            HttpMethod::Get => "GET",
+            HttpMethod::Post => "POST",
+        };
+
+        log::info!("[HTTP] {} {}", method_str, self.url);
+
+        let mut request = match self.method {
+            HttpMethod::Get => client.get(&self.url),
+            HttpMethod::Post => client.post(&self.url),
+        };
+
+        // 添加自定义请求头
+        for (key, value) in &self.headers {
+            request = request.header(key.as_str(), value.as_str());
+        }
+
+        // 设置超时
+        if let Some(timeout) = self.timeout {
+            request = request.timeout(timeout);
+        }
+
+        let start_time = std::time::Instant::now();
+
+        let response = request.send().await.map_err(|e| {
+            log::error!("[HTTP] 请求失败: {} - {}", self.url, e);
+            format!("HTTP 请求失败: {}", e)
+        })?;
+
+        let elapsed = start_time.elapsed();
+        let status = response.status();
+
+        log::info!(
+            "[HTTP] {} {} - {} ({:.2}ms)",
+            method_str,
+            self.url,
+            status,
+            elapsed.as_secs_f64() * 1000.0
+        );
+
+        Ok(HttpResponse {
+            inner: response,
+            url: self.url,
+        })
+    }
+}
+
+/// HTTP 响应包装器
+pub struct HttpResponse {
+    inner: Response,
+    url: String,
+}
+
+impl HttpResponse {
+    /// 获取状态码
+    pub fn status(&self) -> reqwest::StatusCode {
+        self.inner.status()
+    }
+
+    /// 检查是否成功 (2xx)
+    pub fn is_success(&self) -> bool {
+        self.inner.status().is_success()
+    }
+
+    /// 获取响应体为字节
+    pub async fn bytes(self) -> Result<Vec<u8>, String> {
+        let url = self.url.clone();
+        let bytes = self.inner.bytes().await.map_err(|e| {
+            log::error!("[HTTP] 读取响应体失败: {} - {}", url, e);
+            format!("读取响应体失败: {}", e)
+        })?;
+        Ok(bytes.to_vec())
+    }
+
+    /// 获取响应体为文本
+    pub async fn text(self) -> Result<String, String> {
+        let url = self.url.clone();
+        let bytes = self.bytes().await?;
+        let text = String::from_utf8_lossy(&bytes).to_string();
+        log::debug!("[HTTP] 响应体大小: {} bytes ({})", bytes.len(), url);
+        Ok(text)
+    }
+
+    /// 获取响应体并解析为 JSON
+    pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T, String> {
+        let url = self.url.clone();
+        let text = self.text().await?;
+        serde_json::from_str(&text).map_err(|e| {
+            log::error!(
+                "[HTTP] JSON 解析失败: {} - {}, 响应前500字符: {}",
+                url,
+                e,
+                &text.chars().take(500).collect::<String>()
+            );
+            format!("JSON 解析失败: {}", e)
+        })
+    }
+
+    /// 获取内部 Response(用于需要直接访问的场景,如流式下载)
+    #[allow(dead_code)]
+    pub fn into_inner(self) -> Response {
+        self.inner
+    }
+
+    /// 获取 Content-Length
+    #[allow(dead_code)]
+    pub fn content_length(&self) -> Option<u64> {
+        self.inner.content_length()
+    }
+
+    /// 获取响应头
+    #[allow(dead_code)]
+    pub fn headers(&self) -> &reqwest::header::HeaderMap {
+        self.inner.headers()
+    }
+}
+
+// ==================== 便捷函数 ====================
+
+/// 发送 GET 请求
+#[allow(dead_code)]
+pub async fn get(url: &str) -> Result<HttpResponse, String> {
+    HttpRequest::get(url).send().await
+}
+
+/// 发送 GET 请求并获取文本
+#[allow(dead_code)]
+pub async fn get_text(url: &str) -> Result<String, String> {
+    get(url).await?.text().await
+}
+
+/// 发送 GET 请求并解析 JSON
+#[allow(dead_code)]
+pub async fn get_json<T: serde::de::DeserializeOwned>(url: &str) -> Result<T, String> {
+    get(url).await?.json().await
+}

+ 4 - 0
src-tauri/src/utils/mod.rs

@@ -1,4 +1,8 @@
+pub mod http;
 pub mod shell;
 
+#[allow(unused_imports)]
+pub use http::*;
+
 #[allow(unused_imports)]
 pub use shell::*;