From 7f70415c00c8ae527f70a13c3073ea908ef5c547 Mon Sep 17 00:00:00 2001 From: yezi6690-cell Date: Thu, 6 Aug 2026 14:16:59 +0800 Subject: [PATCH 1/2] fix: stabilize Tauri desktop startup --- .../src-tauri/src/cli_install.rs | 5 ++ apps/omninova-tauri/src-tauri/src/lib.rs | 58 +++++++++++++--- apps/omninova-tauri/src-tauri/tauri.conf.json | 2 + apps/omninova-tauri/src/App.css | 37 +++++++++++ apps/omninova-tauri/src/main.tsx | 66 +++++++++++++++++-- 5 files changed, 154 insertions(+), 14 deletions(-) diff --git a/apps/omninova-tauri/src-tauri/src/cli_install.rs b/apps/omninova-tauri/src-tauri/src/cli_install.rs index 4e5ebcf5..3748926e 100644 --- a/apps/omninova-tauri/src-tauri/src/cli_install.rs +++ b/apps/omninova-tauri/src-tauri/src/cli_install.rs @@ -9,6 +9,8 @@ use tauri::AppHandle; use tauri::Manager; const MARKER_LINE: &str = "# OmniNova CLI (PATH)"; +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -94,6 +96,8 @@ fn same_executable_as_bundle(src: &Path, dst: &Path) -> bool { #[cfg(windows)] fn windows_add_user_path(dir: &Path) -> Result<(), String> { + use std::os::windows::process::CommandExt; + let d = dir.to_string_lossy().replace('\'', "''"); let ps = format!( "$d = [System.IO.Path]::GetFullPath('{d}'); \ @@ -105,6 +109,7 @@ fn windows_add_user_path(dir: &Path) -> Result<(), String> { d = d ); let output = StdCommand::new("powershell") + .creation_flags(CREATE_NO_WINDOW) .args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", &ps]) .output() .map_err(|e| format!("无法执行 PowerShell:{e}"))?; diff --git a/apps/omninova-tauri/src-tauri/src/lib.rs b/apps/omninova-tauri/src-tauri/src/lib.rs index f2e3d24f..69b1aaf1 100644 --- a/apps/omninova-tauri/src-tauri/src/lib.rs +++ b/apps/omninova-tauri/src-tauri/src/lib.rs @@ -136,8 +136,28 @@ struct AppState { const EMBEDDED_AGENT_BROWSER_BIN_ENV: &str = "OMNINOVA_AGENT_BROWSER_BIN"; const WEBVIEW2_DATA_DIR_ENV: &str = "OMNINOVA_WEBVIEW2_DATA_DIR"; +const OPEN_DEVTOOLS_ENV: &str = "OMNINOVA_OPEN_DEVTOOLS"; const WEBVIEW2_LOCK_SCAN_MAX_DEPTH: usize = 4; const WEBVIEW2_LOCK_SCAN_MAX_RESULTS: usize = 32; +#[cfg(target_os = "windows")] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +#[cfg(target_os = "windows")] +fn hide_std_command_window(command: &mut StdCommand) { + use std::os::windows::process::CommandExt; + command.creation_flags(CREATE_NO_WINDOW); +} + +#[cfg(not(target_os = "windows"))] +fn hide_std_command_window(_command: &mut StdCommand) {} + +#[cfg(target_os = "windows")] +fn hide_tokio_command_window(command: &mut tokio::process::Command) { + command.creation_flags(CREATE_NO_WINDOW); +} + +#[cfg(not(target_os = "windows"))] +fn hide_tokio_command_window(_command: &mut tokio::process::Command) {} #[derive(Debug)] struct WebviewStartupDiagnostics { @@ -221,7 +241,9 @@ fn collect_webview_lock_like_files(root: &Path) -> Vec { #[cfg(target_os = "windows")] fn windows_process_counts(current_pid: u32) -> (usize, usize) { - let Ok(output) = StdCommand::new("tasklist") + let mut command = StdCommand::new("tasklist"); + hide_std_command_window(&mut command); + let Ok(output) = command .args(["/FO", "CSV", "/NH"]) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -368,7 +390,9 @@ fn is_working_agent_browser_binary(path: &std::path::Path) -> bool { if !path.is_file() { return false; } - let Ok(output) = StdCommand::new(path) + let mut command = StdCommand::new(path); + hide_std_command_window(&mut command); + let Ok(output) = command .arg("--version") .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -1160,7 +1184,9 @@ async fn check_browser_dep() -> Result { #[tauri::command] async fn install_browser_dep() -> Result { - let npm_out = tokio::process::Command::new("npm") + let mut npm_command = tokio::process::Command::new("npm"); + hide_tokio_command_window(&mut npm_command); + let npm_out = npm_command .args(["install", "-g", "agent-browser"]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -1174,7 +1200,9 @@ async fn install_browser_dep() -> Result { let agent_browser_cmd = detect_agent_browser_binary() .unwrap_or_else(|| PathBuf::from("agent-browser")); - let chromium_out = tokio::process::Command::new(&agent_browser_cmd) + let mut chromium_command = tokio::process::Command::new(&agent_browser_cmd); + hide_tokio_command_window(&mut chromium_command); + let chromium_out = chromium_command .arg("install") .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -1191,7 +1219,9 @@ async fn install_browser_dep() -> Result { } async fn check_command_installed(bin: &str, version_flag: &str) -> DepStatusPayload { - match tokio::process::Command::new(bin) + let mut command = tokio::process::Command::new(bin); + hide_tokio_command_window(&mut command); + match command .arg(version_flag) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -2850,7 +2880,12 @@ pub fn run() { ); #[cfg(debug_assertions)] { - window.open_devtools(); + let open_devtools = std::env::var(OPEN_DEVTOOLS_ENV) + .map(|value| matches!(value.trim(), "1" | "true" | "TRUE")) + .unwrap_or(false); + if open_devtools { + window.open_devtools(); + } } } @@ -2894,8 +2929,15 @@ pub fn run() { Ok(()) }) - .build(tauri::generate_context!()) - .expect("error while building tauri application"); + .build(tauri::generate_context!()); + + let app = match app { + Ok(app) => app, + Err(error) => { + eprintln!("[app-startup] application_build_failed error={error}"); + return; + } + }; app.run(|app_handle, event| { #[cfg(target_os = "macos")] diff --git a/apps/omninova-tauri/src-tauri/tauri.conf.json b/apps/omninova-tauri/src-tauri/tauri.conf.json index ac051fa1..c84d3f42 100644 --- a/apps/omninova-tauri/src-tauri/tauri.conf.json +++ b/apps/omninova-tauri/src-tauri/tauri.conf.json @@ -11,6 +11,8 @@ "app": { "windows": [ { + "label": "main", + "create": false, "title": "OmniNova Claw", "width": 1080, "height": 720, diff --git a/apps/omninova-tauri/src/App.css b/apps/omninova-tauri/src/App.css index e0c661cf..f29e1342 100644 --- a/apps/omninova-tauri/src/App.css +++ b/apps/omninova-tauri/src/App.css @@ -4,6 +4,43 @@ overflow: hidden; } +.startup-error { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; + background: var(--surface-cream); + color: var(--text-primary); +} + +.startup-error__card { + width: min(560px, 100%); + padding: 28px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-lg); + background: var(--surface-elevated); + box-shadow: var(--shadow-md); +} + +.startup-error__card h1 { + margin: 0 0 12px; + font-size: 24px; +} + +.startup-error__card p { + margin: 0 0 16px; + color: var(--text-muted); +} + +.startup-error__card code { + display: block; + margin-bottom: 18px; + padding: 12px; + border-radius: var(--radius-sm); + background: rgba(44, 40, 36, 0.06); + overflow-wrap: anywhere; +} + /* ========== Skills gallery (visual card grid) ========== */ .skill-switch { flex-shrink: 0; diff --git a/apps/omninova-tauri/src/main.tsx b/apps/omninova-tauri/src/main.tsx index bef5202a..1f24d508 100644 --- a/apps/omninova-tauri/src/main.tsx +++ b/apps/omninova-tauri/src/main.tsx @@ -1,10 +1,64 @@ -import { StrictMode } from 'react' +import { Component, StrictMode, type ErrorInfo, type ReactNode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' -createRoot(document.getElementById('root')!).render( - - - , -) +interface StartupErrorBoundaryState { + errorMessage: string | null +} + +class StartupErrorBoundary extends Component< + { children: ReactNode }, + StartupErrorBoundaryState +> { + state: StartupErrorBoundaryState = { errorMessage: null } + + static getDerivedStateFromError(error: unknown): StartupErrorBoundaryState { + return { + errorMessage: + error instanceof Error ? error.message : '桌面界面初始化时发生未知错误。', + } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error('[app-startup] render_failed', { + message: error.message, + componentStack: info.componentStack, + }) + } + + render() { + if (this.state.errorMessage) { + return ( +
+
+

OmniNova 启动失败

+

桌面界面未能正常初始化,请重试。若问题持续,请查看启动日志。

+ {this.state.errorMessage} + +
+
+ ) + } + + return this.props.children + } +} + +const rootElement = document.getElementById('root') + +if (!rootElement) { + console.error('[app-startup] root_element_missing') + document.body.innerHTML = + '

OmniNova 启动失败

页面容器缺失,请重新启动应用。

' +} else { + createRoot(rootElement).render( + + + + + , + ) +} From ee9581edf86459d7a4ef3f7882d04c3e51bcfb4e Mon Sep 17 00:00:00 2001 From: yezi6690-cell Date: Thu, 6 Aug 2026 14:36:48 +0800 Subject: [PATCH 2/2] fix: harden Feishu reply outbound flow --- .../src/channels/adapters/outbound.rs | 316 ++++++++++++++++-- .../src/gateway/feishu_worker.rs | 16 +- crates/omninova-core/src/gateway/mod.rs | 25 +- 3 files changed, 308 insertions(+), 49 deletions(-) diff --git a/crates/omninova-core/src/channels/adapters/outbound.rs b/crates/omninova-core/src/channels/adapters/outbound.rs index b3dab215..c198637d 100644 --- a/crates/omninova-core/src/channels/adapters/outbound.rs +++ b/crates/omninova-core/src/channels/adapters/outbound.rs @@ -290,11 +290,44 @@ impl TokenCache { const TOKEN_EXPIRY_SAFETY_MARGIN_SECS: u64 = 60; +#[derive(Debug, Clone, PartialEq, Eq)] +struct PlatformErrorDetails { + code: i64, + message: String, + log_id_present: bool, +} + +fn parse_platform_error(body: &serde_json::Value) -> PlatformErrorDetails { + PlatformErrorDetails { + code: body.get("code").and_then(|value| value.as_i64()).unwrap_or(-1), + message: body + .get("msg") + .and_then(|value| value.as_str()) + .unwrap_or("unknown") + .to_string(), + log_id_present: body.get("log_id").is_some() + || body.pointer("/error/log_id").is_some() + || body.pointer("/data/log_id").is_some(), + } +} + +fn safe_platform_message(message: &str, sensitive_values: &[&str]) -> String { + let truncated = message + .chars() + .filter(|character| !character.is_control()) + .take(200) + .collect::(); + sensitive_values + .iter() + .filter(|value| !value.is_empty()) + .fold(truncated, |safe, value| safe.replace(value, "[REDACTED]")) +} + #[derive(Clone)] struct PlatformOutboundSender { provider: &'static str, channel: ChannelKind, - api_base_url: &'static str, + api_base_url: String, app_id: String, app_secret: String, client: Client, @@ -305,7 +338,7 @@ impl PlatformOutboundSender { fn new( provider: &'static str, channel: ChannelKind, - api_base_url: &'static str, + api_base_url: impl Into, app_id: String, app_secret: String, token_cache: Arc, @@ -313,7 +346,7 @@ impl PlatformOutboundSender { Self { provider, channel, - api_base_url, + api_base_url: api_base_url.into(), app_id, app_secret, client: Client::new(), @@ -434,15 +467,17 @@ impl PlatformOutboundSender { text.len() ); + let encoded_message_id = urlencoding::encode(message_id); let url = format!( "{}/im/v1/messages/{}/reply", - self.api_base_url, message_id + self.api_base_url, encoded_message_id ); let response = match self .client .post(&url) .bearer_auth(&token) + .header("Content-Type", "application/json; charset=utf-8") .json(&json!({ "msg_type": "text", "content": json!({ "text": text }).to_string(), @@ -451,12 +486,15 @@ impl PlatformOutboundSender { .await { Ok(response) => response, - Err(e) => { - println!("[{}-outbound] reply_message_failed error={}", self.provider, e); + Err(_) => { + println!( + "[{}-outbound] reply_message_failed http_status=transport_error platform_error_code=unknown message=request_failed log_id_present=false", + self.provider + ); return OutboundResult::failed( self.provider, "reply_message_failed", - &format!("reply request failed: {}", e), + "reply request failed", ); } }; @@ -464,33 +502,28 @@ impl PlatformOutboundSender { let status = response.status(); let body = match response.json::().await { Ok(body) => body, - Err(e) => { - println!("[{}-outbound] reply_message_failed invalid_response", self.provider); + Err(_) => { + println!( + "[{}-outbound] reply_message_failed http_status={} platform_error_code=unknown message=invalid_response log_id_present=false", + self.provider, + status.as_u16() + ); return OutboundResult::failed( self.provider, "reply_message_failed", - &format!("reply response invalid: {}", e), + "reply response was invalid", ); } }; - let code = body.get("code").and_then(|v| v.as_i64()).unwrap_or(-1); - let platform_msg = body.get("msg").and_then(|v| v.as_str()).unwrap_or("unknown"); - // Check for log_id in both top-level and error object - let log_id_present = body - .get("log_id") - .is_some() - || body - .get("error") - .and_then(|e| e.get("log_id")) - .is_some(); + let error = parse_platform_error(&body); let platform_message_id = body .pointer("/data/message_id") .and_then(serde_json::Value::as_str) .filter(|id| !id.trim().is_empty()) .map(String::from); - if code == 0 { + if status.is_success() && error.code == 0 { println!( "[{}-outbound] reply_message_ok platform_message_id_present={}", self.provider, @@ -501,18 +534,22 @@ impl PlatformOutboundSender { platform_message_id.unwrap_or_else(|| "accepted".to_string()), ) } else { + let platform_msg = safe_platform_message( + &error.message, + &[&token, &self.app_secret, message_id], + ); println!( "[{}-outbound] reply_message_failed http_status={} platform_error_code={} message={} log_id_present={}", self.provider, status.as_u16(), - code, + error.code, platform_msg, - log_id_present + error.log_id_present ); OutboundResult::failed( self.provider, - &format!("platform_error_{}", code), - platform_msg, + &format!("platform_error_{}", error.code), + &platform_msg, ) } } @@ -547,6 +584,7 @@ impl PlatformOutboundSender { self.api_base_url )) .bearer_auth(&token) + .header("Content-Type", "application/json; charset=utf-8") .json(&json!({ "receive_id": target.chat_id, "msg_type": "text", @@ -579,15 +617,14 @@ impl PlatformOutboundSender { } }; - let code = body.get("code").and_then(|v| v.as_i64()).unwrap_or(-1); - let platform_msg = body.get("msg").and_then(|v| v.as_str()).unwrap_or("unknown"); + let error = parse_platform_error(&body); let platform_message_id = body .pointer("/data/message_id") .and_then(serde_json::Value::as_str) .filter(|id| !id.trim().is_empty()) .map(String::from); - if code == 0 { + if status.is_success() && error.code == 0 { println!( "[{}-outbound] send_text_ok platform_message_id_present={}", self.provider, @@ -595,17 +632,28 @@ impl PlatformOutboundSender { ); OutboundResult::success(self.provider, platform_message_id.unwrap_or_else(|| "accepted".to_string())) } else { + let platform_msg = safe_platform_message( + &error.message, + &[ + &token, + &self.app_secret, + &target.chat_id, + target.message_id.as_deref().unwrap_or(""), + target.user_id.as_deref().unwrap_or(""), + ], + ); println!( - "[{}-outbound] send_text_failed http_status={} platform_error_code={} message={}", + "[{}-outbound] send_text_failed http_status={} platform_error_code={} message={} log_id_present={}", self.provider, status.as_u16(), - code, - platform_msg + error.code, + platform_msg, + error.log_id_present ); OutboundResult::failed( self.provider, - &format!("platform_error_{}", code), - platform_msg, + &format!("platform_error_{}", error.code), + &platform_msg, ) } } @@ -801,6 +849,208 @@ impl ChannelOutboundSender for LarkOutboundSender { #[cfg(test)] mod tests { use super::*; + use axum::extract::{Path, State}; + use axum::http::{HeaderMap, StatusCode}; + use axum::routing::post; + use axum::{Json, Router}; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + #[derive(Clone)] + struct TestApiState { + reply_succeeds: bool, + reply_calls: Arc, + create_calls: Arc, + reply_content_is_string: Arc, + reply_authorized: Arc, + } + + struct TestApiServer { + base_url: String, + state: TestApiState, + task: tokio::task::JoinHandle<()>, + } + + impl Drop for TestApiServer { + fn drop(&mut self) { + self.task.abort(); + } + } + + async fn test_token_endpoint() -> Json { + Json(json!({ + "code": 0, + "tenant_access_token": "test-tenant-token", + "expire": 3600 + })) + } + + async fn test_reply_endpoint( + State(state): State, + Path(_message_id): Path, + headers: HeaderMap, + Json(body): Json, + ) -> (StatusCode, Json) { + state.reply_calls.fetch_add(1, Ordering::SeqCst); + state.reply_content_is_string.store( + body.get("content").is_some_and(|value| value.is_string()), + Ordering::SeqCst, + ); + state.reply_authorized.store( + headers + .get("authorization") + .and_then(|value| value.to_str().ok()) + == Some("Bearer test-tenant-token"), + Ordering::SeqCst, + ); + if state.reply_succeeds { + ( + StatusCode::OK, + Json(json!({ "code": 0, "msg": "ok", "data": { "message_id": "reply-result" } })), + ) + } else { + ( + StatusCode::BAD_REQUEST, + Json(json!({ + "code": 230006, + "msg": "Bot ability is not activated.", + "error": { "log_id": "test-log-id" } + })), + ) + } + } + + async fn test_create_endpoint( + State(state): State, + Json(_body): Json, + ) -> Json { + state.create_calls.fetch_add(1, Ordering::SeqCst); + Json(json!({ "code": 0, "msg": "ok", "data": { "message_id": "create-result" } })) + } + + async fn spawn_test_api(reply_succeeds: bool) -> TestApiServer { + let state = TestApiState { + reply_succeeds, + reply_calls: Arc::new(AtomicUsize::new(0)), + create_calls: Arc::new(AtomicUsize::new(0)), + reply_content_is_string: Arc::new(AtomicBool::new(false)), + reply_authorized: Arc::new(AtomicBool::new(false)), + }; + let app = Router::new() + .route( + "/auth/v3/tenant_access_token/internal", + post(test_token_endpoint), + ) + .route( + "/im/v1/messages/{message_id}/reply", + post(test_reply_endpoint), + ) + .route("/im/v1/messages", post(test_create_endpoint)) + .with_state(state.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test API"); + let address = listener.local_addr().expect("test API address"); + let task = tokio::spawn(async move { + let _ = axum::serve(listener, app).await; + }); + TestApiServer { + base_url: format!("http://{address}"), + state, + task, + } + } + + fn real_sender_for_test(server: &TestApiServer) -> PlatformOutboundSender { + PlatformOutboundSender::new( + "feishu", + ChannelKind::Feishu, + server.base_url.clone(), + "test-app-id".to_string(), + "test-app-secret".to_string(), + Arc::new(TokenCache::new()), + ) + } + + fn reply_target(message_id: Option<&str>) -> ReplyTarget { + ReplyTarget { + channel: ChannelKind::Feishu, + chat_id: "test-chat-id".to_string(), + message_id: message_id.map(ToString::to_string), + user_id: Some("test-user-id".to_string()), + } + } + + #[tokio::test] + async fn message_id_prefers_reply_and_success_skips_create() { + let server = spawn_test_api(true).await; + let result = real_sender_for_test(&server) + .send_text_reply(&reply_target(Some("test-message-id")), "hello") + .await; + + assert!(result.ok); + assert_eq!(server.state.reply_calls.load(Ordering::SeqCst), 1); + assert_eq!(server.state.create_calls.load(Ordering::SeqCst), 0); + assert!(server.state.reply_content_is_string.load(Ordering::SeqCst)); + assert!(server.state.reply_authorized.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn reply_failure_falls_back_to_create() { + let server = spawn_test_api(false).await; + let result = real_sender_for_test(&server) + .send_text_reply(&reply_target(Some("test-message-id")), "hello") + .await; + + assert!(result.ok); + assert_eq!(server.state.reply_calls.load(Ordering::SeqCst), 1); + assert_eq!(server.state.create_calls.load(Ordering::SeqCst), 1); + assert_eq!(result.platform_message_id.as_deref(), Some("create-result")); + } + + #[tokio::test] + async fn missing_message_id_uses_create_message() { + let server = spawn_test_api(true).await; + let result = real_sender_for_test(&server) + .send_text_reply(&reply_target(None), "hello") + .await; + + assert!(result.ok); + assert_eq!(server.state.reply_calls.load(Ordering::SeqCst), 0); + assert_eq!(server.state.create_calls.load(Ordering::SeqCst), 1); + } + + #[test] + fn parses_platform_error_code_message_and_log_id() { + let parsed = parse_platform_error(&json!({ + "code": 230006, + "msg": "Bot ability is not activated.", + "error": { "log_id": "private-log-id" } + })); + + assert_eq!(parsed.code, 230006); + assert_eq!(parsed.message, "Bot ability is not activated."); + assert!(parsed.log_id_present); + } + + #[test] + fn outbound_error_log_message_redacts_known_sensitive_values() { + let forbidden = [ + "tenant-token", + "app-secret", + "verification-token", + "encrypt-key", + "chat-id", + "open-id", + "user-id", + "message-id", + ]; + let raw = forbidden.join(" "); + let safe = safe_platform_message(&raw, &forbidden); + + for value in forbidden { + assert!(!safe.contains(value)); + } + } #[tokio::test] async fn mock_sender_records_messages() { diff --git a/crates/omninova-core/src/gateway/feishu_worker.rs b/crates/omninova-core/src/gateway/feishu_worker.rs index f4aff025..850ea331 100644 --- a/crates/omninova-core/src/gateway/feishu_worker.rs +++ b/crates/omninova-core/src/gateway/feishu_worker.rs @@ -915,8 +915,10 @@ async fn send_reply_with_outbox( println!("[{}-outbox] failed to audit: {}", channel_name, e); } println!( - "[{}-outbox] audit outbound_id={} reply_kind=llm_final platform_message_id={}", - channel_name, outbox.outbound_id, platform_msg_id + "[{}-outbox] audit outbound_id={} reply_kind=llm_final platform_message_id_present={}", + channel_name, + outbox.outbound_id, + !platform_msg_id.is_empty() ); } _ => { @@ -967,8 +969,10 @@ async fn send_reply_with_outbox( println!("[{}-outbox] failed to mark sent: {}", channel_name, e); } println!( - "[{}-outbox] sent outbound_id={} platform_message_id={}", - channel_name, outbound_id, platform_msg_id + "[{}-outbox] sent outbound_id={} platform_message_id_present={}", + channel_name, + outbound_id, + !platform_msg_id.is_empty() ); } Ok(Ok(None)) => { @@ -1613,8 +1617,8 @@ async fn deliver_platform_reply_and_record( if let Some(ref msg_id) = outbound_result.platform_message_id { OutboundMsgCache::global().record_outbound(channel_name, msg_id).await; println!( - "[{}-worker] recorded_outbound_message_id={}", - channel_name, msg_id + "[{}-worker] recorded_outbound_message_id=true", + channel_name ); } } diff --git a/crates/omninova-core/src/gateway/mod.rs b/crates/omninova-core/src/gateway/mod.rs index 3be77190..7af4c652 100644 --- a/crates/omninova-core/src/gateway/mod.rs +++ b/crates/omninova-core/src/gateway/mod.rs @@ -4986,8 +4986,10 @@ async fn http_channel_webhook( if let Some(incoming_msg_id) = message_id.as_ref() { let channel_str = channel_name.clone(); if OutboundMsgCache::global().is_our_message(&channel_str, incoming_msg_id).await { - println!("[{}-webhook] skip_self_message reason=outbound_message_id_match message_id={}", - channel_name, incoming_msg_id); + println!( + "[{}-webhook] skip_self_message reason=outbound_message_id_match message_id_present=true", + channel_name + ); return Ok(Json(serde_json::json!({ "ok": true, "accepted": true, @@ -5687,14 +5689,17 @@ async fn deliver_platform_reply( ); // Record outbound message_id for self-message filtering - if result.ok && result.platform_message_id.is_some() { - let msg_id = result.platform_message_id.as_ref().unwrap(); - let channel_str = format!("{:?}", inbound.channel).to_lowercase(); - OutboundMsgCache::global().record_outbound(&channel_str, msg_id).await; - println!( - "[{}-outbound] recorded_outbound_message_id message_id={} ttl_secs=1800", - channel_name_for_log, msg_id - ); + if result.ok { + if let Some(msg_id) = result.platform_message_id.as_ref() { + let channel_str = format!("{:?}", inbound.channel).to_lowercase(); + OutboundMsgCache::global() + .record_outbound(&channel_str, msg_id) + .await; + println!( + "[{}-outbound] recorded_outbound_message_id=true ttl_secs=1800", + channel_name_for_log + ); + } } Some(result)