//! 经 Rust 发起的 `/api/v1` HTTP 请求(可选 AES 加解密)。 use std::collections::HashMap; use std::time::Duration; use chrono::Utc; use rand::Rng; use tauri::{AppHandle, Emitter, Manager}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde_json::Value; use crate::api_crypto::{self, ENCRYPTED_HEADER}; use crate::app_config; use crate::auth_session::AuthSession; use crate::device_serial; use crate::oem_context; pub async fn request_json( method: &str, path: &str, body: Option<&str>, extra_headers: HashMap, ) -> Result<(u16, Value), String> { let path = if path.starts_with('/') { path.to_string() } else { format!("/{path}") }; let base = app_config::api_base().trim_end_matches('/').to_string(); let url = format!("{base}/api/v1{path}"); let client = reqwest::Client::builder() .timeout(Duration::from_secs(60)) .build() .map_err(|e| e.to_string())?; let method = reqwest::Method::from_bytes(method.to_uppercase().as_bytes()) .map_err(|e| format!("无效 HTTP 方法: {e}"))?; let aes_key = api_crypto::aes_key_from_env(); let mut req = client.request(method, &url); let mut headers = HeaderMap::new(); headers.insert( HeaderName::from_static("content-type"), HeaderValue::from_static("application/json"), ); if let Ok(val) = HeaderValue::from_str(device_serial::device_serial()) { headers.insert(HeaderName::from_static("device_serial"), val); } oem_context::apply_install_headers(&mut headers); for (k, v) in extra_headers { if let (Ok(name), Ok(val)) = ( HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v), ) { headers.insert(name, val); } } if let Some(key) = aes_key { headers.insert( HeaderName::from_static("x-aiclient-encrypted"), HeaderValue::from_static("1"), ); if let Some(raw) = body.filter(|s| !s.is_empty()) { let wire = api_crypto::wrap_encrypted(raw.as_bytes(), &key)?; req = req.body(wire); } } else if let Some(raw) = body { req = req.body(raw.to_string()); } req = req.headers(headers); let res = req.send().await.map_err(|e| { if e.is_connect() { println!("无法连接服务器,请确认后端已启动"); "无法连接服务器,请确认后端已启动".to_string() } else { println!("e: {}", e); e.to_string() } })?; let status = res.status().as_u16(); println!("status: {}", status); let encrypted = res .headers() .get(ENCRYPTED_HEADER) .and_then(|v| v.to_str().ok()) == Some("1"); let text = res.text().await.map_err(|e| e.to_string())?; let json_text = if encrypted { let key = aes_key.ok_or_else(|| "响应已加密但未配置 AICLIENT_API_AES_KEY".to_string())?; let plain = api_crypto::unwrap_encrypted(&text, &key)?; String::from_utf8(plain).map_err(|e| format!("响应 UTF-8 无效: {e}"))? } else { text }; let value: Value = serde_json::from_str(&json_text).unwrap_or(Value::String(json_text.clone())); Ok((status, value)) } const HEARTBEAT_INTERVAL_SECS: u64 = 30; /// 与 pythonbackend `heartbeat_time_bucket` 一致:UTC ``%Y-%m-%d %H``。 fn heartbeat_time_bucket() -> String { Utc::now().format("%Y-%m-%d %H").to_string() } /// MD5(时间桶 + verify_code),小写十六进制。 fn compute_server_verify_code(verify_code: &str) -> String { let raw = format!("{}{}", heartbeat_time_bucket(), verify_code); format!("{:x}", md5::compute(raw.as_bytes())) } fn random_verify_code() -> String { const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let mut rng = rand::thread_rng(); (0..32) .map(|_| CHARS[rng.gen_range(0..CHARS.len())] as char) .collect() } fn response_server_verify_code(value: &Value) -> Option<&str> { value .get("data") .and_then(|d| d.get("server_verify_code")) .and_then(|v| v.as_str()) } /// 已登录时向服务端发送心跳(校验设备序列号与会员状态)。 pub async fn send_heartbeat(token: &str) -> Result<(), String> { let token = token.trim(); if token.is_empty() { return Ok(()); } let verify_code = random_verify_code(); let body = serde_json::json!({ "verify_code": verify_code }).to_string(); let expected = compute_server_verify_code(&verify_code); let mut headers = HashMap::new(); headers.insert( "Authorization".to_string(), format!("Bearer {token}"), ); let (status, value) = request_json("POST", "/auth/heartbeat", Some(&body), headers).await?; if status == 401 { return Err("未登录或登录已过期".into()); } let server_code = response_server_verify_code(&value); if server_code != Some(expected.as_str()) { return Err("会员已过期".into()); } if value.get("ok").and_then(|v| v.as_bool()) == Some(false) { let msg = value .get("message") .and_then(|m| m.as_str()) .unwrap_or("心跳校验失败"); return Err(msg.to_string()); } Ok(()) } /// 应用启动后每 30 秒发送一次心跳(仅在有 token 时)。 pub fn spawn_heartbeat_loop(app: AppHandle) { tauri::async_runtime::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(HEARTBEAT_INTERVAL_SECS)); interval.tick().await; loop { interval.tick().await; let token = app .try_state::() .map(|s| s.token()) .unwrap_or_default(); if token.trim().is_empty() { continue; } match send_heartbeat(&token).await { Ok(()) => {} Err(e) => { println!("[heartbeat] failed: {e}"); let _ = app.emit("auth-heartbeat-failed", e); } } } }); }