diff --git a/src/command/adapters/websocket.rs b/src/command/adapters/websocket.rs index 3bf5034..148d867 100644 --- a/src/command/adapters/websocket.rs +++ b/src/command/adapters/websocket.rs @@ -66,6 +66,7 @@ impl OutputAdapter for WebSocketOutputAdapter { outbounds.push(WsOutbound::Error { code: error.code, message: error.message, + timestamp: Some(crate::protocol::now_timestamp()), }); return outbounds; } @@ -77,7 +78,7 @@ impl OutputAdapter for WebSocketOutputAdapter { id: response.request_id.to_string(), content: msg.content.clone(), role: "assistant".to_string(), - attachments: Vec::new(), subagent_task_id: None, + attachments: Vec::new(), subagent_task_id: None, timestamp: Some(crate::protocol::now_timestamp()), }, MessageKind::Notification => { // 根据元数据判断具体类型 @@ -97,7 +98,7 @@ impl OutputAdapter for WebSocketOutputAdapter { id: response.request_id.to_string(), content: msg.content.clone(), role: "assistant".to_string(), - attachments: Vec::new(), subagent_task_id: None, + attachments: Vec::new(), subagent_task_id: None, timestamp: Some(crate::protocol::now_timestamp()), }, } } else if let Some(session_id) = response.metadata.get("session_id") { @@ -136,7 +137,7 @@ impl OutputAdapter for WebSocketOutputAdapter { id: response.request_id.to_string(), content: msg.content.clone(), role: "assistant".to_string(), - attachments: Vec::new(), subagent_task_id: None, + attachments: Vec::new(), subagent_task_id: None, timestamp: Some(crate::protocol::now_timestamp()), }, } } else if let Some(sessions_json) = response.metadata.get("sessions") { @@ -154,7 +155,7 @@ impl OutputAdapter for WebSocketOutputAdapter { id: response.request_id.to_string(), content: msg.content.clone(), role: "assistant".to_string(), - attachments: Vec::new(), subagent_task_id: None, + attachments: Vec::new(), subagent_task_id: None, timestamp: Some(crate::protocol::now_timestamp()), }, } } else if let Some(topics_json) = response.metadata.get("topics") { @@ -173,7 +174,7 @@ impl OutputAdapter for WebSocketOutputAdapter { id: response.request_id.to_string(), content: msg.content.clone(), role: "assistant".to_string(), - attachments: Vec::new(), subagent_task_id: None, + attachments: Vec::new(), subagent_task_id: None, timestamp: Some(crate::protocol::now_timestamp()), }, } } else { @@ -182,19 +183,20 @@ impl OutputAdapter for WebSocketOutputAdapter { id: response.request_id.to_string(), content: msg.content.clone(), role: "assistant".to_string(), - attachments: Vec::new(), subagent_task_id: None, + attachments: Vec::new(), subagent_task_id: None, timestamp: Some(crate::protocol::now_timestamp()), } } } MessageKind::Error => WsOutbound::Error { code: "RESPONSE_ERROR".to_string(), message: msg.content.clone(), + timestamp: Some(crate::protocol::now_timestamp()), }, _ => WsOutbound::AssistantResponse { id: response.request_id.to_string(), content: msg.content.clone(), role: "assistant".to_string(), - attachments: Vec::new(), subagent_task_id: None, + attachments: Vec::new(), subagent_task_id: None, timestamp: Some(crate::protocol::now_timestamp()), }, }; outbounds.push(outbound); diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 98fdb6b..f6222fc 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -235,7 +235,8 @@ async fn handle_socket(ws: WebSocket, state: Arc) { tracing::warn!(error = %e, session_id = %current_session_id, "Failed to handle inbound message"); let _ = sender .send(WsOutbound::Error { - code: "SESSION_ERROR".to_string(), + timestamp: Some(crate::protocol::now_timestamp()), + code:"SESSION_ERROR".to_string(), message: e.to_string(), }) .await; @@ -245,7 +246,8 @@ async fn handle_socket(ws: WebSocket, state: Arc) { tracing::warn!(error = %e, "Failed to parse inbound message"); let _ = sender .send(WsOutbound::Error { - code: "PARSE_ERROR".to_string(), + timestamp: Some(crate::protocol::now_timestamp()), + code:"PARSE_ERROR".to_string(), message: e.to_string(), }) .await; @@ -334,6 +336,7 @@ async fn handle_inbound( // 不是命令,返回错误 let _ = sender .send(WsOutbound::Error { + timestamp: Some(crate::protocol::now_timestamp()), code: "INVALID_COMMAND".to_string(), message: "Invalid command payload".to_string(), }) @@ -343,6 +346,7 @@ async fn handle_inbound( Err(e) => { let _ = sender .send(WsOutbound::Error { + timestamp: Some(crate::protocol::now_timestamp()), code: "PARSE_ERROR".to_string(), message: e.to_string(), }) @@ -707,6 +711,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option Option { @@ -730,6 +736,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option Some(WsOutbound::ToolPending { id: msg.id.clone(), @@ -739,6 +746,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option Option None, } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index fbc2616..dca8b4a 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -2,6 +2,14 @@ pub mod ws_adapter; use serde::{Deserialize, Serialize}; +/// 当前时间戳(秒),用于填充消息的 timestamp 字段 +pub fn now_timestamp() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionSummary { pub session_id: String, @@ -108,6 +116,8 @@ pub enum WsOutbound { attachments: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] subagent_task_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + timestamp: Option, }, #[serde(rename = "tool_call")] ToolCall { @@ -119,6 +129,8 @@ pub enum WsOutbound { role: String, #[serde(default, skip_serializing_if = "Option::is_none")] subagent_task_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + timestamp: Option, }, #[serde(rename = "tool_result")] ToolResult { @@ -131,6 +143,8 @@ pub enum WsOutbound { subagent_task_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] duration_ms: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + timestamp: Option, }, #[serde(rename = "tool_pending")] ToolPending { @@ -142,9 +156,16 @@ pub enum WsOutbound { resume_hint: String, #[serde(default, skip_serializing_if = "Option::is_none")] subagent_task_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + timestamp: Option, }, #[serde(rename = "error")] - Error { code: String, message: String }, + Error { + code: String, + message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + timestamp: Option, + }, #[serde(rename = "task_started")] TaskStarted { task_id: String, diff --git a/src/protocol/ws_adapter.rs b/src/protocol/ws_adapter.rs index 4f2bc71..14a35cb 100644 --- a/src/protocol/ws_adapter.rs +++ b/src/protocol/ws_adapter.rs @@ -93,6 +93,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve role: message.role.clone(), attachments, subagent_task_id: message.metadata.get("subagent_task_id").cloned(), + timestamp: Some(crate::protocol::now_timestamp()), }] } OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall { @@ -109,6 +110,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve content: message.content.clone(), role: message.role.clone(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(), + timestamp: Some(crate::protocol::now_timestamp()), }], OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult { id: message @@ -124,6 +126,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve .metadata .get("tool_duration_ms") .and_then(|v| v.parse().ok()), + timestamp: Some(crate::protocol::now_timestamp()), }], OutboundEventKind::ToolPending => vec![WsOutbound::ToolPending { id: message @@ -136,10 +139,12 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve role: message.role.clone(), resume_hint: TOOL_PENDING_RESUME_HINT.to_string(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(), + timestamp: Some(crate::protocol::now_timestamp()), }], OutboundEventKind::ErrorNotification => vec![WsOutbound::Error { code: "AGENT_ERROR".to_string(), message: message.content.clone(), + timestamp: Some(crate::protocol::now_timestamp()), }], OutboundEventKind::TaskStarted => vec![WsOutbound::TaskStarted { task_id: message.metadata.get("task_id").cloned().unwrap_or_default(), diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index ae0e04b..e4a8d25 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -168,7 +168,7 @@ export function useChat(): UseChatReturn { id: msg.id, role: role as ChatMessage['role'], content: msg.content, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'message', attachments: msg.attachments, subagentTaskId: msg.subagent_task_id, @@ -180,7 +180,7 @@ export function useChat(): UseChatReturn { id: msg.id, role: 'tool', content: msg.content, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'tool_call', toolName: msg.tool_name, toolCallId: msg.tool_call_id, @@ -194,7 +194,7 @@ export function useChat(): UseChatReturn { id: msg.id, role: 'tool', content: msg.content, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'tool_result', toolName: msg.tool_name, toolCallId: msg.tool_call_id, @@ -208,7 +208,7 @@ export function useChat(): UseChatReturn { id: msg.id, role: 'tool', content: `${msg.content}\n\n${msg.resume_hint}`, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'tool_pending', toolName: msg.tool_name, toolCallId: msg.tool_call_id, @@ -220,7 +220,7 @@ export function useChat(): UseChatReturn { id: generateMessageId(), role: 'assistant', content: `Error: ${message.message}`, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'message', } } @@ -394,7 +394,7 @@ export function useChat(): UseChatReturn { id: msg.id, role, content: msg.content, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'message', attachments: msg.attachments, }, @@ -411,7 +411,7 @@ export function useChat(): UseChatReturn { id: msg.id, role: 'tool', content: msg.content, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'tool_call', toolName: msg.tool_name, toolCallId: msg.tool_call_id, @@ -430,7 +430,7 @@ export function useChat(): UseChatReturn { id: msg.id, role: 'tool', content: msg.content, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'tool_result', toolName: msg.tool_name, toolCallId: msg.tool_call_id, @@ -449,7 +449,7 @@ export function useChat(): UseChatReturn { id: msg.id, role: 'tool', content: `${msg.content}\n\n${msg.resume_hint}`, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'tool_pending', toolName: msg.tool_name, toolCallId: msg.tool_call_id, @@ -465,7 +465,7 @@ export function useChat(): UseChatReturn { id: generateMessageId(), role: 'assistant', content: (message as { type: 'execution_cancelled'; message: string }).message, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'message', }, ]) @@ -480,7 +480,7 @@ export function useChat(): UseChatReturn { id: generateMessageId(), role: 'assistant', content: `Error: ${message.message}`, - timestamp: Date.now(), + timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000), type: 'message', }, ]) @@ -513,7 +513,7 @@ export function useChat(): UseChatReturn { id: generateMessageId(), role: 'user', content, - timestamp: Date.now(), + timestamp: Math.floor(Date.now() / 1000), type: 'message', attachments: attachments || [], }, diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index eb5dd27..76312e5 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -43,6 +43,7 @@ export interface AssistantResponse { role: string attachments?: Attachment[] subagent_task_id?: string + timestamp?: number } export interface ToolCall { @@ -54,6 +55,7 @@ export interface ToolCall { content: string role: string subagent_task_id?: string + timestamp?: number } export interface ToolResult { @@ -65,6 +67,7 @@ export interface ToolResult { role: string subagent_task_id?: string duration_ms?: number + timestamp?: number } export interface ToolPending { @@ -76,12 +79,14 @@ export interface ToolPending { role: string resume_hint: string subagent_task_id?: string + timestamp?: number } export interface WsError { type: 'error' code: string message: string + timestamp?: number } export interface TaskStarted {