From 60b294e0ab1ca8c1930473ba5e08f259c4b7b7f9 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Wed, 11 Feb 2026 17:04:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0WOL=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=B7=A8=E6=B5=8F=E8=A7=88=E5=99=A8=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/store.rs | 20 ++++++ src/web/handlers/mod.rs | 112 +++++++++++++++++++++++++++++- src/web/routes.rs | 1 + web/src/api/config.ts | 16 +++++ web/src/components/AtxPopover.vue | 45 +++++++----- 5 files changed, 176 insertions(+), 18 deletions(-) diff --git a/src/config/store.rs b/src/config/store.rs index ce7a1b5b..2a69ade0 100644 --- a/src/config/store.rs +++ b/src/config/store.rs @@ -120,6 +120,26 @@ impl ConfigStore { .execute(pool) .await?; + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS wol_history ( + mac_address TEXT PRIMARY KEY, + updated_at INTEGER NOT NULL + ) + "#, + ) + .execute(pool) + .await?; + + sqlx::query( + r#" + CREATE INDEX IF NOT EXISTS idx_wol_history_updated_at + ON wol_history(updated_at DESC) + "#, + ) + .execute(pool) + .await?; + Ok(()) } diff --git a/src/web/handlers/mod.rs b/src/web/handlers/mod.rs index 47e19fce..10ed79bc 100644 --- a/src/web/handlers/mod.rs +++ b/src/web/handlers/mod.rs @@ -2660,6 +2660,10 @@ pub async fn msd_drive_mkdir( use crate::atx::{AtxState, PowerStatus}; +const WOL_HISTORY_MAX_ENTRIES: i64 = 50; +const WOL_HISTORY_DEFAULT_LIMIT: usize = 5; +const WOL_HISTORY_MAX_LIMIT: usize = 50; + /// ATX state response #[derive(Serialize)] pub struct AtxStateResponse { @@ -2765,11 +2769,78 @@ pub struct WolRequest { pub mac_address: String, } +#[derive(Debug, Deserialize, Default)] +pub struct WolHistoryQuery { + /// Maximum history entries to return + pub limit: Option, +} + +#[derive(Debug, Serialize)] +pub struct WolHistoryEntry { + pub mac_address: String, + pub updated_at: i64, +} + +#[derive(Debug, Serialize)] +pub struct WolHistoryResponse { + pub history: Vec, +} + +fn normalize_wol_mac_address(mac_address: &str) -> String { + let normalized = mac_address.trim().to_uppercase().replace('-', ":"); + + if normalized.len() == 12 && normalized.chars().all(|c| c.is_ascii_hexdigit()) { + let mut mac_with_separator = String::with_capacity(17); + for (index, chunk) in normalized.as_bytes().chunks(2).enumerate() { + if index > 0 { + mac_with_separator.push(':'); + } + mac_with_separator.push(chunk[0] as char); + mac_with_separator.push(chunk[1] as char); + } + mac_with_separator + } else { + normalized + } +} + +async fn record_wol_history(state: &Arc, mac_address: &str) -> Result<()> { + sqlx::query( + r#" + INSERT INTO wol_history (mac_address, updated_at) + VALUES (?1, CAST(strftime('%s', 'now') AS INTEGER)) + ON CONFLICT(mac_address) DO UPDATE SET + updated_at = excluded.updated_at + "#, + ) + .bind(mac_address) + .execute(state.config.pool()) + .await?; + + sqlx::query( + r#" + DELETE FROM wol_history + WHERE mac_address NOT IN ( + SELECT mac_address FROM wol_history + ORDER BY updated_at DESC + LIMIT ?1 + ) + "#, + ) + .bind(WOL_HISTORY_MAX_ENTRIES) + .execute(state.config.pool()) + .await?; + + Ok(()) +} + /// Send Wake-on-LAN magic packet pub async fn atx_wol( State(state): State>, Json(req): Json, ) -> Result> { + let mac_address = normalize_wol_mac_address(&req.mac_address); + // Get WOL interface from config let config = state.config.get(); let interface = if config.atx.wol_interface.is_empty() { @@ -2779,14 +2850,51 @@ pub async fn atx_wol( }; // Send WOL packet - crate::atx::send_wol(&req.mac_address, interface)?; + crate::atx::send_wol(&mac_address, interface)?; + + if let Err(error) = record_wol_history(&state, &mac_address).await { + warn!("Failed to persist WOL history: {}", error); + } Ok(Json(LoginResponse { success: true, - message: Some(format!("WOL packet sent to {}", req.mac_address)), + message: Some(format!("WOL packet sent to {}", mac_address)), })) } +/// Get WOL history +pub async fn atx_wol_history( + State(state): State>, + Query(query): Query, +) -> Result> { + let limit = query + .limit + .unwrap_or(WOL_HISTORY_DEFAULT_LIMIT) + .clamp(1, WOL_HISTORY_MAX_LIMIT); + + let rows: Vec<(String, i64)> = sqlx::query_as( + r#" + SELECT mac_address, updated_at + FROM wol_history + ORDER BY updated_at DESC + LIMIT ?1 + "#, + ) + .bind(limit as i64) + .fetch_all(state.config.pool()) + .await?; + + let history = rows + .into_iter() + .map(|(mac_address, updated_at)| WolHistoryEntry { + mac_address, + updated_at, + }) + .collect(); + + Ok(Json(WolHistoryResponse { history })) +} + // ============================================================================ // Audio Control // ============================================================================ diff --git a/src/web/routes.rs b/src/web/routes.rs index 02e7bfa9..b67c93d3 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -166,6 +166,7 @@ pub fn create_router(state: Arc) -> Router { .route("/atx/status", get(handlers::atx_status)) .route("/atx/power", post(handlers::atx_power)) .route("/atx/wol", post(handlers::atx_wol)) + .route("/atx/wol/history", get(handlers::atx_wol_history)) // Device discovery endpoints .route("/devices/atx", get(handlers::devices::list_atx_devices)) // Extension management endpoints diff --git a/web/src/api/config.ts b/web/src/api/config.ts index d48a5e8c..6198fb08 100644 --- a/web/src/api/config.ts +++ b/web/src/api/config.ts @@ -136,6 +136,15 @@ export const msdConfigApi = { // ===== ATX 配置 API ===== import type { AtxDevices } from '@/types/generated' +export interface WolHistoryEntry { + mac_address: string + updated_at: number +} + +export interface WolHistoryResponse { + history: WolHistoryEntry[] +} + export const atxConfigApi = { /** * 获取 ATX 配置 @@ -166,6 +175,13 @@ export const atxConfigApi = { method: 'POST', body: JSON.stringify({ mac_address: macAddress }), }), + + /** + * 获取 WOL 历史记录(服务端持久化) + * @param limit 返回条数(1-50) + */ + getWolHistory: (limit = 5) => + request(`/atx/wol/history?limit=${Math.max(1, Math.min(50, limit))}`), } // ===== Audio 配置 API ===== diff --git a/web/src/components/AtxPopover.vue b/web/src/components/AtxPopover.vue index 3d70fe78..2583cbc6 100644 --- a/web/src/components/AtxPopover.vue +++ b/web/src/components/AtxPopover.vue @@ -1,5 +1,5 @@