feat: add online device management and desktop settings integration

This commit is contained in:
2026-02-19 17:34:41 +08:00
parent 365ada1a4a
commit 19f53875c9
7 changed files with 1070 additions and 48 deletions

View File

@@ -113,6 +113,53 @@ fn join_api_url(base_url: &str, path: &str) -> String {
format!("{}{}", normalize_base_url(base_url), path)
}
fn sanitize_device_id_component(raw: &str) -> String {
let mut output = String::new();
let mut last_is_dash = false;
for ch in raw.chars() {
let normalized = ch.to_ascii_lowercase();
if normalized.is_ascii_alphanumeric() {
output.push(normalized);
last_is_dash = false;
} else if !last_is_dash {
output.push('-');
last_is_dash = true;
}
}
output.trim_matches('-').to_string()
}
fn build_desktop_client_meta() -> (String, String, String) {
let os = match env::consts::OS {
"windows" => "Windows",
"macos" => "macOS",
"linux" => "Linux",
other => other,
};
let platform = format!("{}-{}", os, env::consts::ARCH);
let host_name = env::var("COMPUTERNAME")
.or_else(|_| env::var("HOSTNAME"))
.unwrap_or_default();
let host_trimmed = host_name.trim();
let device_name = if host_trimmed.is_empty() {
format!("桌面客户端 · {}", platform)
} else {
format!("{} · {}", host_trimmed, platform)
};
let id_seed = if host_trimmed.is_empty() {
platform.clone()
} else {
format!("{}-{}", host_trimmed, platform)
};
let normalized = sanitize_device_id_component(&id_seed);
let device_id = if normalized.is_empty() {
"desktop-client".to_string()
} else {
format!("desktop-{}", normalized)
};
(platform, device_name, device_id)
}
fn fallback_json(status: StatusCode, text: &str) -> Value {
let mut data = Map::new();
data.insert("success".to_string(), Value::Bool(status.is_success()));
@@ -390,9 +437,14 @@ async fn api_login(
password: String,
captcha: Option<String>,
) -> Result<BridgeResponse, String> {
let (platform, device_name, device_id) = build_desktop_client_meta();
let mut body = Map::new();
body.insert("username".to_string(), Value::String(username));
body.insert("password".to_string(), Value::String(password));
body.insert("client_type".to_string(), Value::String("desktop".to_string()));
body.insert("platform".to_string(), Value::String(platform));
body.insert("device_name".to_string(), Value::String(device_name));
body.insert("device_id".to_string(), Value::String(device_id));
if let Some(value) = captcha {
if !value.trim().is_empty() {
body.insert("captcha".to_string(), Value::String(value));
@@ -426,6 +478,50 @@ async fn api_get_profile(
.await
}
#[tauri::command]
async fn api_list_online_devices(
state: tauri::State<'_, ApiState>,
base_url: String,
) -> Result<BridgeResponse, String> {
request_with_optional_csrf(
&state.client,
Method::GET,
&base_url,
"/api/user/online-devices",
None,
false,
)
.await
}
#[tauri::command]
async fn api_kick_online_device(
state: tauri::State<'_, ApiState>,
base_url: String,
session_id: String,
) -> Result<BridgeResponse, String> {
let session = session_id.trim().to_string();
if session.is_empty() {
return Err("会话标识不能为空".to_string());
}
if session.len() > 128 {
return Err("会话标识长度无效".to_string());
}
let api_path = format!(
"/api/user/online-devices/{}/kick",
urlencoding::encode(&session)
);
request_with_optional_csrf(
&state.client,
Method::POST,
&base_url,
&api_path,
Some(Value::Object(Map::new())),
true,
)
.await
}
#[tauri::command]
fn api_save_login_state(
base_url: String,
@@ -1483,6 +1579,8 @@ pub fn run() {
api_load_login_state,
api_clear_login_state,
api_get_profile,
api_list_online_devices,
api_kick_online_device,
api_list_files,
api_logout,
api_search_files,