From a38740af17fa082a77e88fcb79a3fd230f23bba9 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 15:59:00 +0800 Subject: [PATCH 1/8] feat(api): add GET /api/messages list, PATCH /api/messages/{id}, DELETE /api/messages/{id}, GET /api/stats --- src/api/endpoints/messages.rs | 133 ++++++++++++++++++++++++++++++++++ src/api/mod.rs | 5 +- src/core/message.rs | 102 +++++++++++++++++++++++++- 3 files changed, 237 insertions(+), 3 deletions(-) diff --git a/src/api/endpoints/messages.rs b/src/api/endpoints/messages.rs index c1b9d80..6b51aeb 100644 --- a/src/api/endpoints/messages.rs +++ b/src/api/endpoints/messages.rs @@ -120,3 +120,136 @@ pub async fn get_message(id: web::Path) -> impl Responder { pub async fn health_check() -> impl Responder { HttpResponse::Ok().json(serde_json::json!({"status": "ok"})) } + +#[derive(Debug, Serialize, Deserialize)] +pub struct MessageListQuery { + pub offset: Option, + pub limit: Option, + pub status: Option, +} + +pub async fn list_messages(query: web::Query) -> impl Responder { + let offset = query.offset.unwrap_or(0); + let limit = query.limit.unwrap_or(50).min(200); + let status = query.status.as_deref(); + + let result = crate::REPO.with(|repo| { + repo.borrow() + .as_ref() + .map(|r| r.list(status, offset, limit)) + }); + match result { + Some(Ok(messages)) => { + let items: Vec = + messages.into_iter().map(MessageResponse::from).collect(); + HttpResponse::Ok().json(serde_json::json!({ + "messages": items, + "offset": offset, + "limit": limit, + })) + } + Some(Err(e)) => { + tracing::error!(error = %e, "Failed to list messages"); + HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) + } + None => { + tracing::error!("Repository not initialized during list call"); + HttpResponse::InternalServerError() + .json(serde_json::json!({"error": "Repository not initialized"})) + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PatchMessageRequest { + pub status: String, +} + +pub async fn update_message( + id: web::Path, + body: web::Json, +) -> impl Responder { + let new_status = match body.status.as_str() { + "Canceled" | "Failed" => crate::models::message::MessageStatus::Failed, + "Pending" => crate::models::message::MessageStatus::Pending, + _ => { + return HttpResponse::BadRequest() + .json(serde_json::json!({"error": "Invalid status. Use Pending, Failed, or Canceled"})); + } + }; + + let result = crate::REPO.with(|repo| { + repo.borrow() + .as_ref() + .map(|r| r.update_message_status(&id, &new_status)) + }); + match result { + Some(Ok(())) => { + let msg_result = crate::REPO.with(|repo| { + repo.borrow().as_ref().map(|r| r.get(&id)) + }); + match msg_result { + Some(Ok(Some(msg))) => HttpResponse::Ok().json(MessageResponse::from(msg)), + _ => HttpResponse::Ok().json(serde_json::json!({"status": "updated"})), + } + } + Some(Err(e)) => { + tracing::error!(error = %e, message_id = %id.into_inner(), "Failed to update message"); + HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) + } + None => { + tracing::error!("Repository not initialized during update call"); + HttpResponse::InternalServerError() + .json(serde_json::json!({"error": "Repository not initialized"})) + } + } +} + +pub async fn delete_message(id: web::Path) -> impl Responder { + let result = crate::REPO.with(|repo| { + repo.borrow().as_ref().map(|r| r.delete(&id)) + }); + match result { + Some(Ok(true)) => HttpResponse::Ok().json(serde_json::json!({"deleted": true})), + Some(Ok(false)) => { + HttpResponse::NotFound().json(serde_json::json!({"error": "Message not found"})) + } + Some(Err(e)) => { + tracing::error!(error = %e, message_id = %id.into_inner(), "Failed to delete message"); + HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) + } + None => { + tracing::error!("Repository not initialized during delete call"); + HttpResponse::InternalServerError() + .json(serde_json::json!({"error": "Repository not initialized"})) + } + } +} + +pub async fn get_stats() -> impl Responder { + let result = crate::REPO.with(|repo| { + let r = repo.borrow(); + let repo_ref = r.as_ref()?; + let pending = repo_ref.count_by_status("Pending").unwrap_or(0); + let sending = repo_ref.count_by_status("Sending").unwrap_or(0); + let sent = repo_ref.count_by_status("Sent").unwrap_or(0); + let failed = repo_ref.count_by_status("Failed").unwrap_or(0); + Some((pending, sending, sent, failed)) + }); + match result { + Some((pending, sending, sent, failed)) => { + HttpResponse::Ok().json(serde_json::json!({ + "pending": pending, + "sending": sending, + "sent": sent, + "failed": failed, + "total": pending + sending + sent + failed, + })) + } + None => { + tracing::error!("Repository not initialized during stats call"); + HttpResponse::InternalServerError() + .json(serde_json::json!({"error": "Repository not initialized"})) + } + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index a5fc8c5..d95eca1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -8,9 +8,10 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/api") - .service(web::resource("/messages").route(web::post().to(messages::create_message))) - .service(web::resource("/messages/{id}").route(web::get().to(messages::get_message))) + .service(web::resource("/messages").route(web::get().to(messages::list_messages)).route(web::post().to(messages::create_message))) + .service(web::resource("/messages/{id}").route(web::get().to(messages::get_message)).route(web::patch().to(messages::update_message)).route(web::delete().to(messages::delete_message))) .service(web::resource("/health").route(web::get().to(messages::health_check))) + .service(web::resource("/stats").route(web::get().to(messages::get_stats))) .route("/auth/verify", web::post().to(auth_verify)), ) .route("/ws/client", web::get().to(ws_client::ws_client)) diff --git a/src/core/message.rs b/src/core/message.rs index a236522..288925b 100644 --- a/src/core/message.rs +++ b/src/core/message.rs @@ -1,10 +1,43 @@ -use crate::models::message::{Message, MessageStatus}; +use crate::models::message::{Message, MessageStatus, MessageType}; use rusqlite::{Connection, Result}; pub struct MessageRepository { conn: Connection, } +fn read_message_row( + row: (String, String, String, String, String, i64, i64, u32), +) -> Option { + let (id, mt, content, recipient, status_str, created_at_secs, updated_at_secs, retry_count) = row; + let message_type = match mt.as_str() { + "Text" => MessageType::Text, + "Image" => MessageType::Image, + "File" => MessageType::File, + _ => return None, + }; + let status = match status_str.as_str() { + "Pending" => MessageStatus::Pending, + "Sending" => MessageStatus::Sending, + "Sent" => MessageStatus::Sent, + "Failed" => MessageStatus::Failed, + _ => return None, + }; + let created_at = + std::time::UNIX_EPOCH + std::time::Duration::from_secs(created_at_secs as u64); + let updated_at = + std::time::UNIX_EPOCH + std::time::Duration::from_secs(updated_at_secs as u64); + Some(Message { + id, + message_type, + content, + recipient, + status, + created_at, + updated_at, + retry_count, + }) +} + impl MessageRepository { pub fn new(path: &str) -> Result { let conn = Connection::open(path)?; @@ -195,4 +228,71 @@ impl MessageRepository { )?; Ok(()) } + + pub fn list( + &self, + status: Option<&str>, + offset: usize, + limit: usize, + ) -> Result> { + let mut messages = Vec::new(); + if let Some(s) = status { + let mut stmt = self.conn.prepare( + "SELECT id, message_type, content, recipient, status, created_at, updated_at, retry_count FROM messages WHERE status = ?1 ORDER BY created_at DESC LIMIT ?2 OFFSET ?3", + )?; + let rows = stmt.query_map(rusqlite::params![s, limit, offset], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, i64>(5)?, + row.get::<_, i64>(6)?, + row.get::<_, u32>(7)?, + )) + })?; + for row in rows { + if let Some(msg) = read_message_row(row?) { + messages.push(msg); + } + } + } else { + let mut stmt = self.conn.prepare( + "SELECT id, message_type, content, recipient, status, created_at, updated_at, retry_count FROM messages ORDER BY created_at DESC LIMIT ?1 OFFSET ?2", + )?; + let rows = stmt.query_map(rusqlite::params![limit, offset], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, String>(4)?, + row.get::<_, i64>(5)?, + row.get::<_, i64>(6)?, + row.get::<_, u32>(7)?, + )) + })?; + for row in rows { + if let Some(msg) = read_message_row(row?) { + messages.push(msg); + } + } + } + Ok(messages) + } + + pub fn count_by_status(&self, status: &str) -> Result { + let count: i64 = self.conn.query_row( + "SELECT COUNT(*) FROM messages WHERE status = ?1", + rusqlite::params![status], + |row| row.get(0), + )?; + Ok(count as usize) + } + + pub fn delete(&self, id: &str) -> Result { + let affected = self.conn.execute("DELETE FROM messages WHERE id = ?1", rusqlite::params![id])?; + Ok(affected > 0) + } } From 1da90dfba19fc69540b02be69a5582e6179cceba Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 15:59:44 +0800 Subject: [PATCH 2/8] =?UTF-8?q?style:=20=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BB=A5=E6=8F=90=E5=8D=87=E5=8F=AF=E8=AF=BB?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一代码格式,包括: - 拆分长行以符合行宽限制 - 调整多行表达式对齐 - 简化冗余的闭包语法 - 统一JSON响应格式 --- src/api/endpoints/messages.rs | 29 ++++++++++++----------------- src/api/mod.rs | 13 +++++++++++-- src/core/message.rs | 20 ++++++++------------ 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/api/endpoints/messages.rs b/src/api/endpoints/messages.rs index 6b51aeb..9f3c28d 100644 --- a/src/api/endpoints/messages.rs +++ b/src/api/endpoints/messages.rs @@ -173,8 +173,9 @@ pub async fn update_message( "Canceled" | "Failed" => crate::models::message::MessageStatus::Failed, "Pending" => crate::models::message::MessageStatus::Pending, _ => { - return HttpResponse::BadRequest() - .json(serde_json::json!({"error": "Invalid status. Use Pending, Failed, or Canceled"})); + return HttpResponse::BadRequest().json( + serde_json::json!({"error": "Invalid status. Use Pending, Failed, or Canceled"}), + ); } }; @@ -185,9 +186,7 @@ pub async fn update_message( }); match result { Some(Ok(())) => { - let msg_result = crate::REPO.with(|repo| { - repo.borrow().as_ref().map(|r| r.get(&id)) - }); + let msg_result = crate::REPO.with(|repo| repo.borrow().as_ref().map(|r| r.get(&id))); match msg_result { Some(Ok(Some(msg))) => HttpResponse::Ok().json(MessageResponse::from(msg)), _ => HttpResponse::Ok().json(serde_json::json!({"status": "updated"})), @@ -206,9 +205,7 @@ pub async fn update_message( } pub async fn delete_message(id: web::Path) -> impl Responder { - let result = crate::REPO.with(|repo| { - repo.borrow().as_ref().map(|r| r.delete(&id)) - }); + let result = crate::REPO.with(|repo| repo.borrow().as_ref().map(|r| r.delete(&id))); match result { Some(Ok(true)) => HttpResponse::Ok().json(serde_json::json!({"deleted": true})), Some(Ok(false)) => { @@ -237,15 +234,13 @@ pub async fn get_stats() -> impl Responder { Some((pending, sending, sent, failed)) }); match result { - Some((pending, sending, sent, failed)) => { - HttpResponse::Ok().json(serde_json::json!({ - "pending": pending, - "sending": sending, - "sent": sent, - "failed": failed, - "total": pending + sending + sent + failed, - })) - } + Some((pending, sending, sent, failed)) => HttpResponse::Ok().json(serde_json::json!({ + "pending": pending, + "sending": sending, + "sent": sent, + "failed": failed, + "total": pending + sending + sent + failed, + })), None => { tracing::error!("Repository not initialized during stats call"); HttpResponse::InternalServerError() diff --git a/src/api/mod.rs b/src/api/mod.rs index d95eca1..e989efc 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -8,8 +8,17 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/api") - .service(web::resource("/messages").route(web::get().to(messages::list_messages)).route(web::post().to(messages::create_message))) - .service(web::resource("/messages/{id}").route(web::get().to(messages::get_message)).route(web::patch().to(messages::update_message)).route(web::delete().to(messages::delete_message))) + .service( + web::resource("/messages") + .route(web::get().to(messages::list_messages)) + .route(web::post().to(messages::create_message)), + ) + .service( + web::resource("/messages/{id}") + .route(web::get().to(messages::get_message)) + .route(web::patch().to(messages::update_message)) + .route(web::delete().to(messages::delete_message)), + ) .service(web::resource("/health").route(web::get().to(messages::health_check))) .service(web::resource("/stats").route(web::get().to(messages::get_stats))) .route("/auth/verify", web::post().to(auth_verify)), diff --git a/src/core/message.rs b/src/core/message.rs index 288925b..cc29dea 100644 --- a/src/core/message.rs +++ b/src/core/message.rs @@ -8,7 +8,8 @@ pub struct MessageRepository { fn read_message_row( row: (String, String, String, String, String, i64, i64, u32), ) -> Option { - let (id, mt, content, recipient, status_str, created_at_secs, updated_at_secs, retry_count) = row; + let (id, mt, content, recipient, status_str, created_at_secs, updated_at_secs, retry_count) = + row; let message_type = match mt.as_str() { "Text" => MessageType::Text, "Image" => MessageType::Image, @@ -22,10 +23,8 @@ fn read_message_row( "Failed" => MessageStatus::Failed, _ => return None, }; - let created_at = - std::time::UNIX_EPOCH + std::time::Duration::from_secs(created_at_secs as u64); - let updated_at = - std::time::UNIX_EPOCH + std::time::Duration::from_secs(updated_at_secs as u64); + let created_at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(created_at_secs as u64); + let updated_at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(updated_at_secs as u64); Some(Message { id, message_type, @@ -229,12 +228,7 @@ impl MessageRepository { Ok(()) } - pub fn list( - &self, - status: Option<&str>, - offset: usize, - limit: usize, - ) -> Result> { + pub fn list(&self, status: Option<&str>, offset: usize, limit: usize) -> Result> { let mut messages = Vec::new(); if let Some(s) = status { let mut stmt = self.conn.prepare( @@ -292,7 +286,9 @@ impl MessageRepository { } pub fn delete(&self, id: &str) -> Result { - let affected = self.conn.execute("DELETE FROM messages WHERE id = ?1", rusqlite::params![id])?; + let affected = self + .conn + .execute("DELETE FROM messages WHERE id = ?1", rusqlite::params![id])?; Ok(affected > 0) } } From c9ea06f318ef21aeec30c2e9d6b6deb3d1adc529 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 16:32:55 +0800 Subject: [PATCH 3/8] fix(onebot): replace unwrap_or(0) with proper error handling in recipient parsing and lock acquisition --- src/adapters/onebot/adapter.rs | 21 ++++++++++++++++----- src/adapters/onebot/ws.rs | 5 ++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/adapters/onebot/adapter.rs b/src/adapters/onebot/adapter.rs index 467b43f..3388004 100644 --- a/src/adapters/onebot/adapter.rs +++ b/src/adapters/onebot/adapter.rs @@ -43,24 +43,35 @@ impl Adapter for OneBotAdapter { fn send_message(&self, message: &Message) -> Result<(), Box> { let segments = internal_message_to_segments(message); - // Determine if group or private based on conversation ID format let (message_type, target_id) = if message.recipient.starts_with("group_") { let gid: i64 = message .recipient .strip_prefix("group_") .and_then(|s| s.parse().ok()) - .unwrap_or(0); + .ok_or(format!( + "Invalid group ID format in recipient: {}", + message.recipient + ))?; ("group", gid) } else if message.recipient.starts_with("private_") { let uid: i64 = message .recipient .strip_prefix("private_") .and_then(|s| s.parse().ok()) - .unwrap_or(0); + .ok_or(format!( + "Invalid private ID format in recipient: {}", + message.recipient + ))?; ("private", uid) } else { - // Try to parse as numeric; default to group - let id: i64 = message.recipient.parse().ok().unwrap_or(0); + let id: i64 = message + .recipient + .parse() + .ok() + .ok_or(format!( + "Unrecognized recipient format: {}", + message.recipient + ))?; ("group", id) }; diff --git a/src/adapters/onebot/ws.rs b/src/adapters/onebot/ws.rs index 46dc8e6..749ffb0 100644 --- a/src/adapters/onebot/ws.rs +++ b/src/adapters/onebot/ws.rs @@ -35,7 +35,10 @@ pub async fn onebot_ws( let (action_tx, mut action_rx) = mpsc::unbounded_channel::(); // Store the sender so the adapter can use it later { - let mut sender = onebot_sender.lock().unwrap(); + let mut sender = onebot_sender.lock().map_err(|e| { + tracing::error!("Failed to acquire OneBot sender lock: {}", e); + actix_web::error::ErrorInternalServerError("Internal server error") + })?; *sender = Some(action_tx); } From 780177fd743d199a3cb590294465ea1d5e924096 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 16:34:40 +0800 Subject: [PATCH 4/8] =?UTF-8?q?refactor(onebot):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=8E=A5=E6=94=B6=E8=80=85=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adapters/onebot/adapter.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/adapters/onebot/adapter.rs b/src/adapters/onebot/adapter.rs index 3388004..a8b944d 100644 --- a/src/adapters/onebot/adapter.rs +++ b/src/adapters/onebot/adapter.rs @@ -64,14 +64,10 @@ impl Adapter for OneBotAdapter { ))?; ("private", uid) } else { - let id: i64 = message - .recipient - .parse() - .ok() - .ok_or(format!( - "Unrecognized recipient format: {}", - message.recipient - ))?; + let id: i64 = message.recipient.parse().ok().ok_or(format!( + "Unrecognized recipient format: {}", + message.recipient + ))?; ("group", id) }; From 6506ea898b1a24efc40080156558a529900a3a94 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 16:52:24 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat(=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86):?= =?UTF-8?q?=20=E5=A2=9E=E5=BC=BA=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为消息发送过程添加详细的错误日志记录,包括适配器名称、消息ID和错误信息 在消息发送失败时更新消息状态为Failed 扩展消息状态枚举以支持Sending和Sent状态 优化OneBot适配器的错误处理和日志记录 --- src/adapters/onebot/adapter.rs | 87 +++++++++++++++++++++++++--------- src/api/endpoints/messages.rs | 9 ++-- src/api/endpoints/ws_client.rs | 15 +++++- src/core/adapter.rs | 13 ++++- 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/adapters/onebot/adapter.rs b/src/adapters/onebot/adapter.rs index a8b944d..c6506fb 100644 --- a/src/adapters/onebot/adapter.rs +++ b/src/adapters/onebot/adapter.rs @@ -48,47 +48,90 @@ impl Adapter for OneBotAdapter { .recipient .strip_prefix("group_") .and_then(|s| s.parse().ok()) - .ok_or(format!( - "Invalid group ID format in recipient: {}", - message.recipient - ))?; + .ok_or_else(|| { + let err = format!( + "Invalid group ID format in recipient: {}", + message.recipient + ); + tracing::error!( + message_id = %message.id, + recipient = %message.recipient, + "{}", + err + ); + err + })?; ("group", gid) } else if message.recipient.starts_with("private_") { let uid: i64 = message .recipient .strip_prefix("private_") .and_then(|s| s.parse().ok()) - .ok_or(format!( - "Invalid private ID format in recipient: {}", - message.recipient - ))?; + .ok_or_else(|| { + let err = format!( + "Invalid private ID format in recipient: {}", + message.recipient + ); + tracing::error!( + message_id = %message.id, + recipient = %message.recipient, + "{}", + err + ); + err + })?; ("private", uid) } else { - let id: i64 = message.recipient.parse().ok().ok_or(format!( - "Unrecognized recipient format: {}", - message.recipient - ))?; + let id: i64 = message.recipient.parse().ok().ok_or_else(|| { + let err = format!("Unrecognized recipient format: {}", message.recipient); + tracing::error!( + message_id = %message.id, + recipient = %message.recipient, + "{}", + err + ); + err + })?; ("group", id) }; let action = protocol::build_send_msg_action(message_type, target_id, &segments, Some(&message.id)); - let sender = self - .sender - .lock() - .map_err(|e| format!("OneBot sender mutex poisoned: {}", e))?; + let sender = self.sender.lock().map_err(|e| { + let err = format!("OneBot sender mutex poisoned: {}", e); + tracing::error!( + message_id = %message.id, + adapter = %self.name, + "{}", + err + ); + err + })?; - sender - .as_ref() - .ok_or("OneBot sender not initialized")? - .send(action) - .map_err(|e| format!("Failed to send OneBot action: {}", e))?; + let sender_ref = sender.as_ref().ok_or_else(|| { + let err = format!("OneBot sender not initialized for adapter '{}'", self.name); + tracing::error!(message_id = %message.id, adapter = %self.name, "{}", err); + err + })?; + + sender_ref.send(action).map_err(|e| { + let err = format!("Failed to send OneBot action: {}", e); + tracing::error!( + message_id = %message.id, + adapter = %self.name, + conversation = %message.recipient, + "{}", + err + ); + err + })?; tracing::info!( message_id = %message.id, conversation = %message.recipient, - "OneBot message sent" + adapter = %self.name, + "OneBot message sent successfully" ); Ok(()) } diff --git a/src/api/endpoints/messages.rs b/src/api/endpoints/messages.rs index 9f3c28d..b062b70 100644 --- a/src/api/endpoints/messages.rs +++ b/src/api/endpoints/messages.rs @@ -170,12 +170,13 @@ pub async fn update_message( body: web::Json, ) -> impl Responder { let new_status = match body.status.as_str() { - "Canceled" | "Failed" => crate::models::message::MessageStatus::Failed, "Pending" => crate::models::message::MessageStatus::Pending, + "Sending" => crate::models::message::MessageStatus::Sending, + "Sent" => crate::models::message::MessageStatus::Sent, + "Canceled" | "Failed" => crate::models::message::MessageStatus::Failed, _ => { - return HttpResponse::BadRequest().json( - serde_json::json!({"error": "Invalid status. Use Pending, Failed, or Canceled"}), - ); + return HttpResponse::BadRequest() + .json(serde_json::json!({"error": "Invalid status. Use Pending, Sending, Sent, Failed, or Canceled"})); } }; diff --git a/src/api/endpoints/ws_client.rs b/src/api/endpoints/ws_client.rs index 806acf4..b57efe2 100644 --- a/src/api/endpoints/ws_client.rs +++ b/src/api/endpoints/ws_client.rs @@ -198,7 +198,20 @@ async fn handle_command( } if let Err(e) = adapter_manager.send_to_adapter(&platform, &message) { - tracing::warn!(platform = %platform, error = %e, "No adapter found for platform"); + tracing::error!( + platform = %platform, + message_id = %message.id, + error = %e, + "Failed to send message via adapter" + ); + let _ = crate::REPO.with(|repo| { + if let Some(r) = repo.borrow().as_ref() { + let _ = r.update_message_status( + &message.id, + &crate::models::message::MessageStatus::Failed, + ); + } + }); } let now = message diff --git a/src/core/adapter.rs b/src/core/adapter.rs index e8739ed..7bcd1d2 100644 --- a/src/core/adapter.rs +++ b/src/core/adapter.rs @@ -100,7 +100,18 @@ impl AdapterManager { ) -> Vec>> { self.adapters .iter() - .map(|adapter| adapter.send_message(message)) + .map(|adapter| { + let result = adapter.send_message(message); + if let Err(ref e) = result { + tracing::error!( + adapter = %adapter.name(), + message_id = %message.id, + error = %e, + "Failed to broadcast message to adapter" + ); + } + result + }) .collect() } } From 81642f6884b6f9f7ccfeaa6d8d292d4b472822aa Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 17:00:30 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat(=E6=B6=88=E6=81=AF=E7=8A=B6=E6=80=81):?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E5=8F=96=E6=B6=88=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在消息状态枚举中添加Canceled状态,并更新API端点、核心逻辑中相关的状态匹配处理 --- src/api/endpoints/messages.rs | 3 ++- src/core/message.rs | 3 +++ src/models/message.rs | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/endpoints/messages.rs b/src/api/endpoints/messages.rs index b062b70..19dbe7b 100644 --- a/src/api/endpoints/messages.rs +++ b/src/api/endpoints/messages.rs @@ -173,7 +173,8 @@ pub async fn update_message( "Pending" => crate::models::message::MessageStatus::Pending, "Sending" => crate::models::message::MessageStatus::Sending, "Sent" => crate::models::message::MessageStatus::Sent, - "Canceled" | "Failed" => crate::models::message::MessageStatus::Failed, + "Canceled" => crate::models::message::MessageStatus::Canceled, + "Failed" => crate::models::message::MessageStatus::Failed, _ => { return HttpResponse::BadRequest() .json(serde_json::json!({"error": "Invalid status. Use Pending, Sending, Sent, Failed, or Canceled"})); diff --git a/src/core/message.rs b/src/core/message.rs index cc29dea..0b520d9 100644 --- a/src/core/message.rs +++ b/src/core/message.rs @@ -21,6 +21,7 @@ fn read_message_row( "Sending" => MessageStatus::Sending, "Sent" => MessageStatus::Sent, "Failed" => MessageStatus::Failed, + "Canceled" => MessageStatus::Canceled, _ => return None, }; let created_at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(created_at_secs as u64); @@ -128,6 +129,7 @@ impl MessageRepository { "Sending" => MessageStatus::Sending, "Sent" => MessageStatus::Sent, "Failed" => MessageStatus::Failed, + "Canceled" => MessageStatus::Canceled, _ => return Err(rusqlite::Error::InvalidQuery), }; @@ -181,6 +183,7 @@ impl MessageRepository { "Sending" => MessageStatus::Sending, "Sent" => MessageStatus::Sent, "Failed" => MessageStatus::Failed, + "Canceled" => MessageStatus::Canceled, _ => continue, }; diff --git a/src/models/message.rs b/src/models/message.rs index 8fdc615..cc0f0d0 100644 --- a/src/models/message.rs +++ b/src/models/message.rs @@ -14,6 +14,7 @@ pub enum MessageStatus { Sending, Sent, Failed, + Canceled, } #[derive(Debug, Serialize, Deserialize)] From 00fc530f82cdf54215e70774788cdf7647f8f73f Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 17:02:39 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix(ws=5Fclient):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84REPO=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除未使用的let绑定以消除编译器警告,保持代码整洁 --- src/api/endpoints/ws_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/endpoints/ws_client.rs b/src/api/endpoints/ws_client.rs index b57efe2..a62b2df 100644 --- a/src/api/endpoints/ws_client.rs +++ b/src/api/endpoints/ws_client.rs @@ -204,7 +204,7 @@ async fn handle_command( error = %e, "Failed to send message via adapter" ); - let _ = crate::REPO.with(|repo| { + crate::REPO.with(|repo| { if let Some(r) = repo.borrow().as_ref() { let _ = r.update_message_status( &message.id, From 07d967fc6a01cb47962b3bc9744d05608c27e100 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Fri, 1 May 2026 17:08:14 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat(=E6=B6=88=E6=81=AF=E7=BB=9F=E8=AE=A1):?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E5=8F=96=E6=B6=88=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=9A=84=E6=B6=88=E6=81=AF=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在消息统计接口中增加对取消状态的消息计数,并更新总计数逻辑 --- src/api/endpoints/messages.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/api/endpoints/messages.rs b/src/api/endpoints/messages.rs index 19dbe7b..c0b7baf 100644 --- a/src/api/endpoints/messages.rs +++ b/src/api/endpoints/messages.rs @@ -233,16 +233,20 @@ pub async fn get_stats() -> impl Responder { let sending = repo_ref.count_by_status("Sending").unwrap_or(0); let sent = repo_ref.count_by_status("Sent").unwrap_or(0); let failed = repo_ref.count_by_status("Failed").unwrap_or(0); - Some((pending, sending, sent, failed)) + let canceled = repo_ref.count_by_status("Canceled").unwrap_or(0); + Some((pending, sending, sent, failed, canceled)) }); match result { - Some((pending, sending, sent, failed)) => HttpResponse::Ok().json(serde_json::json!({ - "pending": pending, - "sending": sending, - "sent": sent, - "failed": failed, - "total": pending + sending + sent + failed, - })), + Some((pending, sending, sent, failed, canceled)) => { + HttpResponse::Ok().json(serde_json::json!({ + "pending": pending, + "sending": sending, + "sent": sent, + "failed": failed, + "canceled": canceled, + "total": pending + sending + sent + failed + canceled, + })) + } None => { tracing::error!("Repository not initialized during stats call"); HttpResponse::InternalServerError()