use std::collections::HashMap; use std::net::SocketAddr; use axum::{ extract::{Path, State}, http::{HeaderMap, StatusCode}, response::Json, routing::{get, post}, Router, }; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter}; use super::engine::DownloadEngine; use super::task::{DownloadTask, TaskStatus}; /// 扩展 HTTP API 服务器 pub struct ExtensionServer; #[derive(Serialize)] struct HealthResponse { ok: bool, version: &'static str, } #[derive(Deserialize)] struct CreateDownloadRequest { url: String, #[serde(default)] filename: Option, #[serde(default)] dir: Option, #[serde(default)] headers: HashMap, } #[derive(Serialize)] struct CreateDownloadResponse { id: String, } #[derive(Serialize)] struct ErrorResponse { error: String, } impl ExtensionServer { /// 启动 HTTP API 服务器(绑定到 127.0.0.1:port) pub async fn start(engine: DownloadEngine, port: u16, secret: String, app_handle: AppHandle) { let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().expect("无效端口"); let app = Router::new() .route("/health", get(health)) .route("/api/downloads", post(create_download).get(list_downloads)) .route("/api/downloads/:id", axum::routing::delete(remove_download)) .with_state(AppState { engine, secret, app_handle, }); let listener = match tokio::net::TcpListener::bind(&addr).await { Ok(l) => l, Err(e) => { crate::logger::log_error("download", &format!("扩展 HTTP 服务启动失败 ({}): {}", addr, e)); return; } }; crate::logger::log_info("download", &format!("扩展 HTTP 服务已启动: http://{}", addr)); if let Err(e) = axum::serve(listener, app).await { crate::logger::log_error("download", &format!("扩展 HTTP 服务异常: {}", e)); } } } #[derive(Clone)] struct AppState { engine: DownloadEngine, secret: String, app_handle: AppHandle, } /// 鉴权检查:如果配置了 secret,校验 Bearer token fn check_auth(headers: &HeaderMap, secret: &str) -> bool { if secret.is_empty() { return true; } if let Some(auth) = headers.get("authorization") { if let Ok(s) = auth.to_str() { if let Some(token) = s.strip_prefix("Bearer ") { return token == secret; } } } false } async fn health(State(state): State) -> Json { let _ = state; // 不需要鉴权 Json(HealthResponse { ok: true, version: "thing-download-engine/1.0", }) } async fn create_download( State(state): State, headers: HeaderMap, Json(req): Json, ) -> Result, (StatusCode, Json)> { if !check_auth(&headers, &state.secret) { return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() }))); } // 去重:同 URL 已有非终态任务(活跃/排队/暂停)时直接返回既有任务, // 避免浏览器重复转发同一下载造成重复下载 if let Some(existing) = state.engine.get_tasks().into_iter().find(|t| { matches!(t.status, TaskStatus::Active | TaskStatus::Queued | TaskStatus::Paused) && t.url == req.url }) { return Ok(Json(CreateDownloadResponse { id: existing.id })); } match state.engine.add_task(req.url, req.filename, req.dir, req.headers, true, None).await { Ok(id) => { // 浏览器扩展发起下载:不再置前主窗口,改为带 task id 通知前端, // 由前端为该任务创建一个专属的一次性下载窗口(不打断主界面) let _ = state.app_handle.emit( crate::constants::events::DOWNLOAD_EXTENSION_ADDED, serde_json::json!({ "id": id }), ); Ok(Json(CreateDownloadResponse { id })) } Err(e) => Err((StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }))), } } async fn list_downloads( State(state): State, headers: HeaderMap, ) -> Result>, (StatusCode, Json)> { if !check_auth(&headers, &state.secret) { return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() }))); } Ok(Json(state.engine.get_tasks())) } async fn remove_download( State(state): State, headers: HeaderMap, Path(id): Path, ) -> Result)> { if !check_auth(&headers, &state.secret) { return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() }))); } match state.engine.remove_task(&id, false) { Ok(()) => { // 通知前端刷新任务列表(扩展删除时前端无从感知,否则列表残留已删除任务) let _ = state.app_handle.emit(crate::constants::events::DOWNLOAD_REMOVED, serde_json::json!({ "id": id })); Ok(StatusCode::NO_CONTENT) } Err(e) => Err((StatusCode::NOT_FOUND, Json(ErrorResponse { error: e }))), } }