From c70995fbc99d58b22c695afef7c75e876ee1a601 Mon Sep 17 00:00:00 2001 From: "fengchuanhn@gmail.com" Date: Sun, 17 May 2026 22:05:34 +0800 Subject: [PATCH] 11 --- .env.example | 2 + src-tauri/Cargo.lock | 104 ++++++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 4 ++ src-tauri/src/api_client.rs | 95 +++++++++++++++++++++++++++++ src-tauri/src/api_crypto.rs | 92 ++++++++++++++++++++++++++++ src-tauri/src/app_config.rs | 30 +++------ src-tauri/src/commands/api.rs | 63 +++++++++++++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/js_runtime/mod.rs | 47 +++------------ src-tauri/src/lib.rs | 3 + src-tauri/src/nodejs.rs | 40 +++--------- src/api/client.js | 84 ++++++++++++-------------- src/config/videoPipeline.js | 7 +-- src/views/LocalConfigView.vue | 4 +- 14 files changed, 428 insertions(+), 148 deletions(-) create mode 100644 src-tauri/src/api_client.rs create mode 100644 src-tauri/src/api_crypto.rs create mode 100644 src-tauri/src/commands/api.rs diff --git a/.env.example b/.env.example index 7a05834..4228329 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ # Tauri Rust 请求后端基址(拉取 appConfig、脚本等) # AICLIENT_API_BASE=http://127.0.0.1:8001 + + # 应用配置在 pythonbackend 的 MySQL 表 desktop_configs(name/value), # 登录后自动同步;Node 流水线通过环境变量 AICLIENT_CFG_ 读取。 # 常用键示例:LLM_API_KEY、LLM_API_URL、LLM_MODEL_ID、ASR_MODE、DASHSCOPE_API_KEY、OSS_CONFIG(JSON) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6b5d431..e78c39a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -547,6 +582,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -689,6 +734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -731,6 +777,15 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.23.0" @@ -1430,6 +1485,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gio" version = "0.18.4" @@ -1939,6 +2004,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2578,6 +2652,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.3.5" @@ -2801,6 +2881,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3984,12 +4076,14 @@ dependencies = [ name = "tauri-app" version = "0.1.0" dependencies = [ + "aes-gcm", "async-trait", "base64 0.22.1", "chrono", "dirs 5.0.1", "env_logger", "futures-util", + "hex", "hmac", "log", "mime_guess", @@ -4715,6 +4809,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8d833c0..d65520d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,10 @@ tokio = { version = "1", features = ["full"] } futures-util = "0.3" async-trait = "0.1" +# API AES-256-GCM(与 pythonbackend 一致) +aes-gcm = "0.10" +hex = "0.4" + # HTTP client reqwest = { version = "0.12", default-features = false, features = [ "json", diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs new file mode 100644 index 0000000..fcc5291 --- /dev/null +++ b/src-tauri/src/api_client.rs @@ -0,0 +1,95 @@ +//! 经 Rust 发起的 `/api/v1` HTTP 请求(可选 AES 加解密)。 + +use std::collections::HashMap; +use std::time::Duration; + +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +use serde_json::Value; + +use crate::api_crypto::{self, ENCRYPTED_HEADER}; +use crate::app_config; + +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"), + ); + 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() { + "无法连接服务器,请确认后端已启动".to_string() + } else { + e.to_string() + } + })?; + + let status = res.status().as_u16(); + 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)) +} diff --git a/src-tauri/src/api_crypto.rs b/src-tauri/src/api_crypto.rs new file mode 100644 index 0000000..ac81fd1 --- /dev/null +++ b/src-tauri/src/api_crypto.rs @@ -0,0 +1,92 @@ +//! AES-256-GCM,与 pythonbackend `app/core/aes_crypto.py` 一致。 + +use rand::RngCore; + +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Nonce, +}; +use base64::Engine; +use serde_json::Value; + +pub const ENCRYPTED_HEADER: &str = "X-AICLIENT-Encrypted"; +const ENVELOPE_VERSION: i64 = 1; + +pub fn parse_aes_key(raw: &str) -> Option<[u8; 32]> { + let s = raw.trim(); + if s.is_empty() { + return None; + } + let bytes = if s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()) { + hex::decode(s).ok()? + } else { + base64::engine::general_purpose::STANDARD + .decode(s) + .ok()? + }; + if bytes.len() != 32 { + return None; + } + let mut key = [0u8; 32]; + key.copy_from_slice(&bytes); + Some(key) +} + +pub fn aes_key_from_env() -> Option<[u8; 32]> { + let s="98cde5e64ee0da6ed0c5842f43a950aa0d8ae5087c43c8d8ee5d10bd8d571bb4"; + parse_aes_key(&s) +} + +pub fn encrypt_bytes(plaintext: &[u8], key: &[u8; 32]) -> Result { + let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| e.to_string())?; + let mut nonce_bytes = [0u8; 12]; + rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|e| format!("加密失败: {e}"))?; + let mut combined = nonce_bytes.to_vec(); + combined.extend(ciphertext); + Ok(base64::engine::general_purpose::STANDARD.encode(&combined)) +} + +pub fn decrypt_payload(b64_payload: &str, key: &[u8; 32]) -> Result, String> { + let raw = base64::engine::general_purpose::STANDARD + .decode(b64_payload.trim()) + .map_err(|e| format!("Base64 解码失败: {e}"))?; + if raw.len() < 13 { + return Err("密文过短".into()); + } + let (nonce_bytes, ciphertext) = raw.split_at(12); + let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| e.to_string())?; + let nonce = Nonce::from_slice(nonce_bytes); + cipher + .decrypt(nonce, ciphertext) + .map_err(|e| format!("解密失败: {e}")) +} + +pub fn wrap_encrypted(plaintext: &[u8], key: &[u8; 32]) -> Result { + let payload = encrypt_bytes(plaintext, key)?; + let envelope = serde_json::json!({ + "v": ENVELOPE_VERSION, + "payload": payload, + }); + serde_json::to_string(&envelope).map_err(|e| e.to_string()) +} + +pub fn unwrap_encrypted(body: &str, key: &[u8; 32]) -> Result, String> { + let v: Value = serde_json::from_str(body).map_err(|e| format!("信封 JSON 无效: {e}"))?; + let version = v.get("v").and_then(|x| x.as_i64()); + let payload = v + .get("payload") + .and_then(|x| x.as_str()) + .ok_or_else(|| "缺少 payload".to_string())?; + if version != Some(ENVELOPE_VERSION) { + return Err("不支持的加密版本".into()); + } + decrypt_payload(payload, key) +} + +pub fn crypto_enabled() -> bool { + aes_key_from_env().is_some() +} diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index ff21ce1..4ba1661 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -237,34 +237,20 @@ impl AppConfig { } async fn fetch_from_server(token: &str) -> Result, AppConfigError> { - let base = api_base().trim_end_matches('/').to_string(); - let url = format!("{base}/api/v1/appConfig"); + let mut headers = HashMap::new(); + headers.insert( + "Authorization".to_string(), + format!("Bearer {}", token.trim()), + ); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| AppConfigError::Fetch(e.to_string()))?; - - let res = client - .get(&url) - .header("Authorization", format!("Bearer {}", token.trim())) - .send() + let (status, v) = crate::api_client::request_json("GET", "/appConfig", None, headers) .await - .map_err(|e| AppConfigError::Fetch(e.to_string()))?; + .map_err(|e| AppConfigError::Fetch(e))?; - let status = res.status(); - let body = res - .text() - .await - .map_err(|e| AppConfigError::Fetch(e.to_string()))?; - - if status == reqwest::StatusCode::UNAUTHORIZED { + if status == 401 { return Err(AppConfigError::Fetch("未认证或令牌已失效".into())); } - let v: Value = - serde_json::from_str(&body).map_err(|e| AppConfigError::Fetch(format!("响应非 JSON: {e}")))?; - let ok = v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false); if !ok { let msg = v diff --git a/src-tauri/src/commands/api.rs b/src-tauri/src/commands/api.rs new file mode 100644 index 0000000..4b0699a --- /dev/null +++ b/src-tauri/src/commands/api.rs @@ -0,0 +1,63 @@ +//! 前端 API 代理:由 Rust 发起 HTTP,并与 Python 端 AES 加解密对齐。 + +use std::collections::HashMap; + +use serde_json::Value; + +use crate::api_client; + +fn default_method() -> String { + "GET".into() +} + +/// 将 HTTP 结果整理为前端 `apiRequest` 约定结构。 +fn normalize_response(status: u16, value: Value) -> Value { + if let Some(obj) = value.as_object() { + if obj.contains_key("ok") { + return value; + } + } + + if status == 401 { + return serde_json::json!({ "ok": false, "message": "未登录或登录已过期" }); + } + if status == 403 { + let msg = value + .get("detail") + .and_then(|d| d.as_str()) + .unwrap_or("无权限执行此操作"); + return serde_json::json!({ "ok": false, "message": msg }); + } + if status == 422 { + let msg = value + .get("detail") + .and_then(|d| d.as_array()) + .and_then(|a| a.first()) + .and_then(|e| e.get("msg")) + .and_then(|m| m.as_str()) + .unwrap_or("请求参数无效"); + return serde_json::json!({ "ok": false, "message": msg }); + } + + let fallback = format!("请求失败({status})"); + let message = value + .get("message") + .and_then(|m| m.as_str()) + .unwrap_or(fallback.as_str()); + serde_json::json!({ "ok": false, "message": message }) +} + +#[tauri::command] +pub async fn api_request( + path: String, + method: Option, + body: Option, + headers: Option>, +) -> Result { + let method = method.unwrap_or_else(|| default_method()); + let headers = headers.unwrap_or_default(); + let (status, value) = + api_client::request_json(&method, &path, body.as_deref(), headers).await?; + + Ok(normalize_response(status, value)) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 24d34c1..218d2b5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod api; pub mod app_config; pub mod auth; pub mod nodejs; diff --git a/src-tauri/src/js_runtime/mod.rs b/src-tauri/src/js_runtime/mod.rs index 7c45736..4529e2a 100644 --- a/src-tauri/src/js_runtime/mod.rs +++ b/src-tauri/src/js_runtime/mod.rs @@ -7,6 +7,7 @@ pub mod native; +use std::collections::HashMap; use std::sync::Arc; use rquickjs::{async_with, promise::Promise, AsyncContext, AsyncRuntime, CatchResultExt}; @@ -47,27 +48,11 @@ fn api_base() -> String { } async fn fetch_script_source(script_name: &str) -> Result { - let base = api_base().trim_end_matches('/').to_string(); let q = urlencoding::encode(script_name); - let url = format!("{base}/api/v1/quickjs-scripts?name={q}"); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .build() - .map_err(|e| JsError::FetchScript(e.to_string()))?; - - let res = client - .get(&url) - .send() + let path = format!("/quickjs-scripts?name={q}"); + let (_status, v) = crate::api_client::request_json("GET", &path, None, HashMap::new()) .await - .map_err(|e| JsError::FetchScript(e.to_string()))?; - - let body = res - .text() - .await - .map_err(|e| JsError::FetchScript(e.to_string()))?; - - let v: Value = - serde_json::from_str(&body).map_err(|e| JsError::FetchScript(format!("响应非 JSON: {e}")))?; + .map_err(|e| JsError::FetchScript(e))?; let ok = v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false); if !ok { @@ -242,26 +227,10 @@ pub async fn run_script_source( /// 当前服务端 `scripts/quickjs` 目录下的 `.js` 文件名列表。 pub async fn list_scripts() -> Result, JsError> { - let base = api_base().trim_end_matches('/').to_string(); - let url = format!("{base}/api/v1/quickjs-scripts/list"); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| JsError::FetchScript(e.to_string()))?; - - let res = client - .get(&url) - .send() - .await - .map_err(|e| JsError::FetchScript(e.to_string()))?; - - let body = res - .text() - .await - .map_err(|e| JsError::FetchScript(e.to_string()))?; - - let v: Value = - serde_json::from_str(&body).map_err(|e| JsError::FetchScript(format!("响应非 JSON: {e}")))?; + let (_status, v) = + crate::api_client::request_json("GET", "/quickjs-scripts/list", None, HashMap::new()) + .await + .map_err(|e| JsError::FetchScript(e))?; if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) { let msg = v diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7201912..2102a47 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +pub mod api_client; +pub mod api_crypto; pub mod app_config; pub mod local_config; pub mod asr; @@ -43,6 +45,7 @@ pub fn run() { })) .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ + commands::api::api_request, commands::auth::sync_auth_session, commands::app_config::get_app_config, commands::app_config::get_app_config_last_message, diff --git a/src-tauri/src/nodejs.rs b/src-tauri/src/nodejs.rs index 30ac1e6..8b9097e 100644 --- a/src-tauri/src/nodejs.rs +++ b/src-tauri/src/nodejs.rs @@ -262,24 +262,11 @@ fn api_base() -> String { } async fn fetch_node_script_source(script_name: &str) -> Result { - let base = api_base().trim_end_matches('/').to_string(); let q = urlencoding::encode(script_name); - let url = format!("{base}/api/v1/nodejs-scripts?name={q}"); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .build() - .map_err(|e| NodeError::FetchScript(e.to_string()))?; - let res = client - .get(&url) - .send() + let path = format!("/nodejs-scripts?name={q}"); + let (_status, v) = crate::api_client::request_json("GET", &path, None, HashMap::new()) .await - .map_err(|e| NodeError::FetchScript(e.to_string()))?; - let body = res - .text() - .await - .map_err(|e| NodeError::FetchScript(e.to_string()))?; - let v: Value = serde_json::from_str(&body) - .map_err(|e| NodeError::FetchScript(format!("响应非 JSON: {e}")))?; + .map_err(|e| NodeError::FetchScript(e))?; if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) { let msg = v .get("message") @@ -297,23 +284,10 @@ async fn fetch_node_script_source(script_name: &str) -> Result Result, NodeError> { - let base = api_base().trim_end_matches('/').to_string(); - let url = format!("{base}/api/v1/nodejs-scripts/list"); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| NodeError::FetchScript(e.to_string()))?; - let res = client - .get(&url) - .send() - .await - .map_err(|e| NodeError::FetchScript(e.to_string()))?; - let body = res - .text() - .await - .map_err(|e| NodeError::FetchScript(e.to_string()))?; - let v: Value = serde_json::from_str(&body) - .map_err(|e| NodeError::FetchScript(format!("响应非 JSON: {e}")))?; + let (_status, v) = + crate::api_client::request_json("GET", "/nodejs-scripts/list", None, HashMap::new()) + .await + .map_err(|e| NodeError::FetchScript(e))?; if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) { let msg = v .get("message") diff --git a/src/api/client.js b/src/api/client.js index 8afb1d3..5485169 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -1,69 +1,59 @@ -/** API 基址:开发走 Vite 代理 `/api`;生产可设 VITE_API_BASE_URL */ +import { invoke } from "@tauri-apps/api/core"; + +/** API 路径前缀(实际 HTTP 由 Rust `api_request` 发出) */ const API_BASE = import.meta.env.VITE_API_BASE_URL || "/api/v1"; -function formatValidationDetail(detail) { - if (!Array.isArray(detail)) { - return "请求参数无效"; - } - const first = detail[0]; - if (first?.msg) { - return String(first.msg); - } - return "请求参数无效"; +function isTauri() { + return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; } /** - * @param {string} path - 相对路径,如 `/auth/register` + * @param {string} path - 相对 `/api/v1` 的路径,如 `/auth/login` * @param {RequestInit} options * @returns {Promise<{ ok: boolean, message: string, data?: unknown }>} */ export async function apiRequest(path, options = {}) { - const url = `${API_BASE}${path}`; - const headers = { - "Content-Type": "application/json", - ...options.headers, - }; - - let res; - try { - res = await fetch(url, { ...options, headers }); - } catch { - return { ok: false, message: "无法连接服务器,请确认后端已启动" }; - } - - let body = null; - try { - body = await res.json(); - } catch { - body = null; - } - - if (res.status === 422) { + if (!isTauri()) { return { ok: false, - message: formatValidationDetail(body?.detail), + message: "API 请求需在 Tauri 桌面端运行(由 Rust 代理并加解密)", }; } - if (body && typeof body.ok === "boolean") { - return body; + const method = (options.method || "GET").toUpperCase(); + let body; + if (options.body != null && options.body !== "") { + body = typeof options.body === "string" ? options.body : JSON.stringify(options.body); } - if (res.status === 401) { - return { ok: false, message: "未登录或登录已过期" }; + const headers = {}; + if (options.headers) { + const h = options.headers; + if (h instanceof Headers) { + h.forEach((value, key) => { + headers[key] = value; + }); + } else { + Object.assign(headers, h); + } } - if (res.status === 403) { - return { - ok: false, - message: body?.detail || "无权限执行此操作", - }; - } + const relPath = path.startsWith("/") ? path : `/${path}`; - return { - ok: false, - message: body?.message || `请求失败(${res.status})`, - }; + try { + const result = await invoke("api_request", { + path: relPath, + method, + body: body ?? null, + headers: Object.keys(headers).length ? headers : null, + }); + if (result && typeof result === "object" && "ok" in result) { + return result; + } + return { ok: false, message: "服务器返回格式异常" }; + } catch (err) { + return { ok: false, message: String(err) }; + } } export { API_BASE }; diff --git a/src/config/videoPipeline.js b/src/config/videoPipeline.js index 65ef4fe..d83386d 100644 --- a/src/config/videoPipeline.js +++ b/src/config/videoPipeline.js @@ -10,15 +10,12 @@ export async function ensureAppConfigReady() { try { const map = await invoke("get_app_config"); if (map && typeof map === "object" && Object.keys(map).length > 0) { - const apiKey = - map.LLM_API_KEY || - map.llm_api_key || - map["llm.api_key"]; + const apiKey =map.BAILIAN_API_KEY ; if (!apiKey) { return { ok: false, message: - "缺少 LLM_API_KEY,请在管理后台 desktop_configs 或侧栏「本地配置」中设置", + "缺少 BAILIAN_API_KEY 配置管理 或侧栏「本地配置」中设置", }; } return { ok: true }; diff --git a/src/views/LocalConfigView.vue b/src/views/LocalConfigView.vue index 513e812..7824366 100644 --- a/src/views/LocalConfigView.vue +++ b/src/views/LocalConfigView.vue @@ -119,10 +119,10 @@ onMounted(() => {

本地配置

-

+