feat: 为 WebSocket 消息添加时间戳字段,确保消息的时间信息可用

This commit is contained in:
ooodc 2026-06-07 09:18:15 +08:00
parent 02339465b6
commit bf66c00950
6 changed files with 64 additions and 22 deletions

View File

@ -66,6 +66,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
outbounds.push(WsOutbound::Error { outbounds.push(WsOutbound::Error {
code: error.code, code: error.code,
message: error.message, message: error.message,
timestamp: Some(crate::protocol::now_timestamp()),
}); });
return outbounds; return outbounds;
} }
@ -77,7 +78,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
id: response.request_id.to_string(), id: response.request_id.to_string(),
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), 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 => { MessageKind::Notification => {
// 根据元数据判断具体类型 // 根据元数据判断具体类型
@ -97,7 +98,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
id: response.request_id.to_string(), id: response.request_id.to_string(),
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), 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") { } 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(), id: response.request_id.to_string(),
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), 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") { } else if let Some(sessions_json) = response.metadata.get("sessions") {
@ -154,7 +155,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
id: response.request_id.to_string(), id: response.request_id.to_string(),
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), 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") { } else if let Some(topics_json) = response.metadata.get("topics") {
@ -173,7 +174,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
id: response.request_id.to_string(), id: response.request_id.to_string(),
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), 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 { } else {
@ -182,19 +183,20 @@ impl OutputAdapter for WebSocketOutputAdapter {
id: response.request_id.to_string(), id: response.request_id.to_string(),
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), 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 { MessageKind::Error => WsOutbound::Error {
code: "RESPONSE_ERROR".to_string(), code: "RESPONSE_ERROR".to_string(),
message: msg.content.clone(), message: msg.content.clone(),
timestamp: Some(crate::protocol::now_timestamp()),
}, },
_ => WsOutbound::AssistantResponse { _ => WsOutbound::AssistantResponse {
id: response.request_id.to_string(), id: response.request_id.to_string(),
content: msg.content.clone(), content: msg.content.clone(),
role: "assistant".to_string(), 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); outbounds.push(outbound);

View File

@ -235,6 +235,7 @@ async fn handle_socket(ws: WebSocket, state: Arc<GatewayState>) {
tracing::warn!(error = %e, session_id = %current_session_id, "Failed to handle inbound message"); tracing::warn!(error = %e, session_id = %current_session_id, "Failed to handle inbound message");
let _ = sender let _ = sender
.send(WsOutbound::Error { .send(WsOutbound::Error {
timestamp: Some(crate::protocol::now_timestamp()),
code:"SESSION_ERROR".to_string(), code:"SESSION_ERROR".to_string(),
message: e.to_string(), message: e.to_string(),
}) })
@ -245,6 +246,7 @@ async fn handle_socket(ws: WebSocket, state: Arc<GatewayState>) {
tracing::warn!(error = %e, "Failed to parse inbound message"); tracing::warn!(error = %e, "Failed to parse inbound message");
let _ = sender let _ = sender
.send(WsOutbound::Error { .send(WsOutbound::Error {
timestamp: Some(crate::protocol::now_timestamp()),
code:"PARSE_ERROR".to_string(), code:"PARSE_ERROR".to_string(),
message: e.to_string(), message: e.to_string(),
}) })
@ -334,6 +336,7 @@ async fn handle_inbound(
// 不是命令,返回错误 // 不是命令,返回错误
let _ = sender let _ = sender
.send(WsOutbound::Error { .send(WsOutbound::Error {
timestamp: Some(crate::protocol::now_timestamp()),
code: "INVALID_COMMAND".to_string(), code: "INVALID_COMMAND".to_string(),
message: "Invalid command payload".to_string(), message: "Invalid command payload".to_string(),
}) })
@ -343,6 +346,7 @@ async fn handle_inbound(
Err(e) => { Err(e) => {
let _ = sender let _ = sender
.send(WsOutbound::Error { .send(WsOutbound::Error {
timestamp: Some(crate::protocol::now_timestamp()),
code: "PARSE_ERROR".to_string(), code: "PARSE_ERROR".to_string(),
message: e.to_string(), message: e.to_string(),
}) })
@ -707,6 +711,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
content: format!("{}\nargs: {}", tool_call.name, tool_call.arguments), content: format!("{}\nargs: {}", tool_call.name, tool_call.arguments),
role: msg.role.clone(), role: msg.role.clone(),
subagent_task_id: None, subagent_task_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
}); });
} }
} }
@ -717,6 +722,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
role: msg.role.clone(), role: msg.role.clone(),
attachments: Vec::new(), attachments: Vec::new(),
subagent_task_id: None, subagent_task_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
}) })
} }
"tool" => { "tool" => {
@ -730,6 +736,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
role: msg.role.clone(), role: msg.role.clone(),
subagent_task_id: None, subagent_task_id: None,
duration_ms: msg.tool_duration_ms, duration_ms: msg.tool_duration_ms,
timestamp: Some(crate::protocol::now_timestamp()),
}), }),
ToolMessageState::PendingUserAction => Some(WsOutbound::ToolPending { ToolMessageState::PendingUserAction => Some(WsOutbound::ToolPending {
id: msg.id.clone(), id: msg.id.clone(),
@ -739,6 +746,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
role: msg.role.clone(), role: msg.role.clone(),
resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(), resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(),
subagent_task_id: None, subagent_task_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
}), }),
} }
} }
@ -748,6 +756,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
role: msg.role.clone(), role: msg.role.clone(),
attachments, attachments,
subagent_task_id: None, subagent_task_id: None,
timestamp: Some(crate::protocol::now_timestamp()),
}), }),
_ => None, _ => None,
} }

View File

@ -2,6 +2,14 @@ pub mod ws_adapter;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSummary { pub struct SessionSummary {
pub session_id: String, pub session_id: String,
@ -108,6 +116,8 @@ pub enum WsOutbound {
attachments: Vec<MediaSummary>, attachments: Vec<MediaSummary>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
subagent_task_id: Option<String>, subagent_task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>,
}, },
#[serde(rename = "tool_call")] #[serde(rename = "tool_call")]
ToolCall { ToolCall {
@ -119,6 +129,8 @@ pub enum WsOutbound {
role: String, role: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
subagent_task_id: Option<String>, subagent_task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>,
}, },
#[serde(rename = "tool_result")] #[serde(rename = "tool_result")]
ToolResult { ToolResult {
@ -131,6 +143,8 @@ pub enum WsOutbound {
subagent_task_id: Option<String>, subagent_task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
duration_ms: Option<u64>, duration_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>,
}, },
#[serde(rename = "tool_pending")] #[serde(rename = "tool_pending")]
ToolPending { ToolPending {
@ -142,9 +156,16 @@ pub enum WsOutbound {
resume_hint: String, resume_hint: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
subagent_task_id: Option<String>, subagent_task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>,
}, },
#[serde(rename = "error")] #[serde(rename = "error")]
Error { code: String, message: String }, Error {
code: String,
message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>,
},
#[serde(rename = "task_started")] #[serde(rename = "task_started")]
TaskStarted { TaskStarted {
task_id: String, task_id: String,

View File

@ -93,6 +93,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
role: message.role.clone(), role: message.role.clone(),
attachments, attachments,
subagent_task_id: message.metadata.get("subagent_task_id").cloned(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()),
}] }]
} }
OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall { OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall {
@ -109,6 +110,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
content: message.content.clone(), content: message.content.clone(),
role: message.role.clone(), role: message.role.clone(),
subagent_task_id: message.metadata.get("subagent_task_id").cloned(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()),
}], }],
OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult { OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult {
id: message id: message
@ -124,6 +126,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
.metadata .metadata
.get("tool_duration_ms") .get("tool_duration_ms")
.and_then(|v| v.parse().ok()), .and_then(|v| v.parse().ok()),
timestamp: Some(crate::protocol::now_timestamp()),
}], }],
OutboundEventKind::ToolPending => vec![WsOutbound::ToolPending { OutboundEventKind::ToolPending => vec![WsOutbound::ToolPending {
id: message id: message
@ -136,10 +139,12 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
role: message.role.clone(), role: message.role.clone(),
resume_hint: TOOL_PENDING_RESUME_HINT.to_string(), resume_hint: TOOL_PENDING_RESUME_HINT.to_string(),
subagent_task_id: message.metadata.get("subagent_task_id").cloned(), subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()),
}], }],
OutboundEventKind::ErrorNotification => vec![WsOutbound::Error { OutboundEventKind::ErrorNotification => vec![WsOutbound::Error {
code: "AGENT_ERROR".to_string(), code: "AGENT_ERROR".to_string(),
message: message.content.clone(), message: message.content.clone(),
timestamp: Some(crate::protocol::now_timestamp()),
}], }],
OutboundEventKind::TaskStarted => vec![WsOutbound::TaskStarted { OutboundEventKind::TaskStarted => vec![WsOutbound::TaskStarted {
task_id: message.metadata.get("task_id").cloned().unwrap_or_default(), task_id: message.metadata.get("task_id").cloned().unwrap_or_default(),

View File

@ -168,7 +168,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role: role as ChatMessage['role'], role: role as ChatMessage['role'],
content: msg.content, content: msg.content,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message', type: 'message',
attachments: msg.attachments, attachments: msg.attachments,
subagentTaskId: msg.subagent_task_id, subagentTaskId: msg.subagent_task_id,
@ -180,7 +180,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role: 'tool', role: 'tool',
content: msg.content, content: msg.content,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_call', type: 'tool_call',
toolName: msg.tool_name, toolName: msg.tool_name,
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
@ -194,7 +194,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role: 'tool', role: 'tool',
content: msg.content, content: msg.content,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_result', type: 'tool_result',
toolName: msg.tool_name, toolName: msg.tool_name,
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
@ -208,7 +208,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role: 'tool', role: 'tool',
content: `${msg.content}\n\n${msg.resume_hint}`, content: `${msg.content}\n\n${msg.resume_hint}`,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_pending', type: 'tool_pending',
toolName: msg.tool_name, toolName: msg.tool_name,
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
@ -220,7 +220,7 @@ export function useChat(): UseChatReturn {
id: generateMessageId(), id: generateMessageId(),
role: 'assistant', role: 'assistant',
content: `Error: ${message.message}`, content: `Error: ${message.message}`,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message', type: 'message',
} }
} }
@ -394,7 +394,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role, role,
content: msg.content, content: msg.content,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message', type: 'message',
attachments: msg.attachments, attachments: msg.attachments,
}, },
@ -411,7 +411,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role: 'tool', role: 'tool',
content: msg.content, content: msg.content,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_call', type: 'tool_call',
toolName: msg.tool_name, toolName: msg.tool_name,
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
@ -430,7 +430,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role: 'tool', role: 'tool',
content: msg.content, content: msg.content,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_result', type: 'tool_result',
toolName: msg.tool_name, toolName: msg.tool_name,
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
@ -449,7 +449,7 @@ export function useChat(): UseChatReturn {
id: msg.id, id: msg.id,
role: 'tool', role: 'tool',
content: `${msg.content}\n\n${msg.resume_hint}`, content: `${msg.content}\n\n${msg.resume_hint}`,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'tool_pending', type: 'tool_pending',
toolName: msg.tool_name, toolName: msg.tool_name,
toolCallId: msg.tool_call_id, toolCallId: msg.tool_call_id,
@ -465,7 +465,7 @@ export function useChat(): UseChatReturn {
id: generateMessageId(), id: generateMessageId(),
role: 'assistant', role: 'assistant',
content: (message as { type: 'execution_cancelled'; message: string }).message, content: (message as { type: 'execution_cancelled'; message: string }).message,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message', type: 'message',
}, },
]) ])
@ -480,7 +480,7 @@ export function useChat(): UseChatReturn {
id: generateMessageId(), id: generateMessageId(),
role: 'assistant', role: 'assistant',
content: `Error: ${message.message}`, content: `Error: ${message.message}`,
timestamp: Date.now(), timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
type: 'message', type: 'message',
}, },
]) ])
@ -513,7 +513,7 @@ export function useChat(): UseChatReturn {
id: generateMessageId(), id: generateMessageId(),
role: 'user', role: 'user',
content, content,
timestamp: Date.now(), timestamp: Math.floor(Date.now() / 1000),
type: 'message', type: 'message',
attachments: attachments || [], attachments: attachments || [],
}, },

View File

@ -43,6 +43,7 @@ export interface AssistantResponse {
role: string role: string
attachments?: Attachment[] attachments?: Attachment[]
subagent_task_id?: string subagent_task_id?: string
timestamp?: number
} }
export interface ToolCall { export interface ToolCall {
@ -54,6 +55,7 @@ export interface ToolCall {
content: string content: string
role: string role: string
subagent_task_id?: string subagent_task_id?: string
timestamp?: number
} }
export interface ToolResult { export interface ToolResult {
@ -65,6 +67,7 @@ export interface ToolResult {
role: string role: string
subagent_task_id?: string subagent_task_id?: string
duration_ms?: number duration_ms?: number
timestamp?: number
} }
export interface ToolPending { export interface ToolPending {
@ -76,12 +79,14 @@ export interface ToolPending {
role: string role: string
resume_hint: string resume_hint: string
subagent_task_id?: string subagent_task_id?: string
timestamp?: number
} }
export interface WsError { export interface WsError {
type: 'error' type: 'error'
code: string code: string
message: string message: string
timestamp?: number
} }
export interface TaskStarted { export interface TaskStarted {