From f16632a1a5ceaa1982575e8d74cc55b353322929 Mon Sep 17 00:00:00 2001 From: AlphaCat Date: Sat, 10 Oct 2026 11:50:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20=E6=9C=AA=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E6=97=B6=E5=8F=91=E5=B8=83=E9=98=9F=E5=88=97?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E4=B8=8D=E5=86=8D=E6=8A=A5=20outbound=20stre?= =?UTF-8?q?am=20is=20offline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 未启用或未连上 Gateway 时,publish_chat_queue_event 把出站流离线当作错误返回, 前端每次排队变化都会 console.warn 一次。改为与 runtime status 发布一致: 离线时视为无需投递直接返回 Ok,真实发送失败仍然上抛。 离线错误文案提取为常量 GATEWAY_OUTBOUND_OFFLINE_ERROR,避免字符串匹配漂移。 Closes #949 --- .../src-tauri/src/services/gateway/chat.rs | 32 +++++++---- .../src/services/gateway/chat_inbox.rs | 6 +- .../src/services/gateway/connection.rs | 14 ++++- .../src-tauri/src/services/gateway/tests.rs | 55 +++++++++++++++++-- 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/crates/agent-gui/src-tauri/src/services/gateway/chat.rs b/crates/agent-gui/src-tauri/src/services/gateway/chat.rs index 8211aa9e4..61c91a2fa 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/chat.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/chat.rs @@ -398,18 +398,26 @@ impl GatewayController { &self, input: GatewayChatQueueEventInput, ) -> Result<(), String> { - self.send_agent_envelope(proto::AgentEnvelope { - request_id: format!("chat-queue-event-{}", Uuid::new_v4()), - timestamp: now_unix_seconds(), - payload: Some(proto::agent_envelope::Payload::ChatQueueEvent( - proto::ChatQueueEvent { - conversation_id: input.conversation_id, - snapshot_json: input.snapshot_json, - revision: input.revision, - }, - )), - }) - .await + let result = self + .send_agent_envelope(build_chat_queue_event_envelope(input)) + .await; + ignore_gateway_outbound_offline(result) + } +} + +pub(crate) fn build_chat_queue_event_envelope( + input: GatewayChatQueueEventInput, +) -> proto::AgentEnvelope { + proto::AgentEnvelope { + request_id: format!("chat-queue-event-{}", Uuid::new_v4()), + timestamp: now_unix_seconds(), + payload: Some(proto::agent_envelope::Payload::ChatQueueEvent( + proto::ChatQueueEvent { + conversation_id: input.conversation_id, + snapshot_json: input.snapshot_json, + revision: input.revision, + }, + )), } } diff --git a/crates/agent-gui/src-tauri/src/services/gateway/chat_inbox.rs b/crates/agent-gui/src-tauri/src/services/gateway/chat_inbox.rs index 53f8c86e8..a5f3e898a 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/chat_inbox.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/chat_inbox.rs @@ -727,11 +727,7 @@ impl GatewayController { .map(chat_run_report_from_entry) .collect(), ); - match self.send_agent_envelope(envelope).await { - Ok(()) => Ok(()), - Err(error) if error.contains("outbound stream is offline") => Ok(()), - Err(error) => Err(error), - } + ignore_gateway_outbound_offline(self.send_agent_envelope(envelope).await) } pub fn release_chat_request_lease( diff --git a/crates/agent-gui/src-tauri/src/services/gateway/connection.rs b/crates/agent-gui/src-tauri/src/services/gateway/connection.rs index 429b9dce9..32d37de9e 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/connection.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/connection.rs @@ -628,7 +628,7 @@ impl GatewayController { .lock() .map_err(|_| "gateway outbound sender lock poisoned".to_string())? .clone() - .ok_or_else(|| "gateway outbound stream is offline".to_string()) + .ok_or_else(|| GATEWAY_OUTBOUND_OFFLINE_ERROR.to_string()) } pub(crate) fn current_outbound_control_sender(&self) -> Result { @@ -879,6 +879,18 @@ pub(crate) fn is_remote_configured(config: &RemoteSettingsPayload) -> bool { !config.gateway_url.trim().is_empty() && !config.token.trim().is_empty() } +pub(crate) const GATEWAY_OUTBOUND_OFFLINE_ERROR: &str = "gateway outbound stream is offline"; + +/// Best-effort publishes (queue snapshots, runtime status) have nothing to +/// deliver while the Gateway is disabled or disconnected; that is the normal +/// state, not a failure. Real send errors still propagate. +pub(crate) fn ignore_gateway_outbound_offline(result: Result<(), String>) -> Result<(), String> { + match result { + Err(error) if error == GATEWAY_OUTBOUND_OFFLINE_ERROR => Ok(()), + other => other, + } +} + pub(crate) fn effective_agent_id(config: &RemoteSettingsPayload) -> Result { let agent_id = config.agent_id.trim(); if agent_id.is_empty() { diff --git a/crates/agent-gui/src-tauri/src/services/gateway/tests.rs b/crates/agent-gui/src-tauri/src/services/gateway/tests.rs index fb91abc14..18cb1f9c8 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/tests.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/tests.rs @@ -1,13 +1,14 @@ use super::{ - attach_stt_secret_sync, build_gateway_runtime_status_envelope, + attach_stt_secret_sync, build_chat_queue_event_envelope, build_gateway_runtime_status_envelope, build_local_settings_update_event_payload, effective_agent_id, gateway_connection_needs_restart, gateway_connection_stale_after, gateway_reconnect_backoff, - history_share_resolve_error_code, is_chat_runtime_wake_request_id, - merge_settings_sync_snapshot, merge_settings_update_into_snapshot, proto, - removed_workspace_project_ids, required_terminal_project_path_key, set_disconnected_status, + history_share_resolve_error_code, ignore_gateway_outbound_offline, + is_chat_runtime_wake_request_id, merge_settings_sync_snapshot, + merge_settings_update_into_snapshot, proto, removed_workspace_project_ids, + required_terminal_project_path_key, set_disconnected_status, GatewayChatQueueEventInput, GatewayChatRequestEvent, GatewayController, GatewayStatusSnapshot, RemoteChatInboxRecord, - GATEWAY_CHAT_LEASE_MS, GATEWAY_CHAT_RUNNING_LEASE_MS, GATEWAY_RECONNECT_MAX, - GATEWAY_RECONNECT_MIN, GATEWAY_RECONNECT_STABLE_AFTER, + GATEWAY_CHAT_LEASE_MS, GATEWAY_CHAT_RUNNING_LEASE_MS, GATEWAY_OUTBOUND_OFFLINE_ERROR, + GATEWAY_RECONNECT_MAX, GATEWAY_RECONNECT_MIN, GATEWAY_RECONNECT_STABLE_AFTER, GATEWAY_RUNTIME_STATUS_REPUBLISH_MAX_AGE, GATEWAY_WEBVIEW_REPORT_FRESH_WINDOW, }; use crate::commands::settings::RemoteSettingsPayload; @@ -904,3 +905,45 @@ fn stalled_webview_status_refreshes_running_ledger_only_inside_grace_window() { ) ); } + +#[test] +fn gateway_offline_is_ignored_for_best_effort_publishes() { + assert_eq!( + ignore_gateway_outbound_offline(Err(GATEWAY_OUTBOUND_OFFLINE_ERROR.to_string())), + Ok(()) + ); + assert_eq!(ignore_gateway_outbound_offline(Ok(())), Ok(())); +} + +#[test] +fn gateway_publish_keeps_real_send_failures() { + for error in [ + "gateway outbound sender lock poisoned", + "gateway outbound control lane is offline", + "gateway outbound lane closed", + ] { + assert_eq!( + ignore_gateway_outbound_offline(Err(error.to_string())), + Err(error.to_string()) + ); + } +} + +#[test] +fn chat_queue_event_envelope_carries_snapshot() { + let envelope = build_chat_queue_event_envelope(GatewayChatQueueEventInput { + conversation_id: "conversation-1".to_string(), + snapshot_json: r#"{"items":[]}"#.to_string(), + revision: 7, + }); + + assert!(envelope.request_id.starts_with("chat-queue-event-")); + match envelope.payload { + Some(proto::agent_envelope::Payload::ChatQueueEvent(event)) => { + assert_eq!(event.conversation_id, "conversation-1"); + assert_eq!(event.snapshot_json, r#"{"items":[]}"#); + assert_eq!(event.revision, 7); + } + other => panic!("unexpected payload: {other:?}"), + } +}