Compare commits
No commits in common. "a40f57fb22bfa6b35c742b1d083810a0f21007b9" and "7f262c9af2225e2211e643fb8257faeebd89b26a" have entirely different histories.
a40f57fb22
...
7f262c9af2
@ -79,7 +79,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: None, user_message_id: None,
|
reasoning_content: None,
|
||||||
},
|
},
|
||||||
MessageKind::Notification => {
|
MessageKind::Notification => {
|
||||||
// 根据元数据判断具体类型
|
// 根据元数据判断具体类型
|
||||||
@ -100,7 +100,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: None, user_message_id: None,
|
reasoning_content: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if let Some(session_id) = response.metadata.get("session_id") {
|
} else if let Some(session_id) = response.metadata.get("session_id") {
|
||||||
@ -140,7 +140,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: None, user_message_id: None,
|
reasoning_content: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if let Some(sessions_json) = response.metadata.get("sessions") {
|
} else if let Some(sessions_json) = response.metadata.get("sessions") {
|
||||||
@ -159,7 +159,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: None, user_message_id: None,
|
reasoning_content: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if let Some(topics_json) = response.metadata.get("topics") {
|
} else if let Some(topics_json) = response.metadata.get("topics") {
|
||||||
@ -179,7 +179,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: None, user_message_id: None,
|
reasoning_content: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -189,7 +189,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: None, user_message_id: None,
|
reasoning_content: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,7 +203,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
attachments: Vec::new(), subagent_task_id: None, topic_id: None, timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: None, user_message_id: None,
|
reasoning_content: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
outbounds.push(outbound);
|
outbounds.push(outbound);
|
||||||
|
|||||||
@ -258,9 +258,7 @@ impl AgentExecutionService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = agent.process(history, Some(&system_prompt_context)).await?;
|
let result = agent.process(history, Some(&system_prompt_context)).await?;
|
||||||
let mut metadata = HashMap::new();
|
let metadata = HashMap::new();
|
||||||
// 把用户消息的 UUID 回传给前端,前端用此更新本地消息 ID,使 todo 点击跳转能匹配
|
|
||||||
metadata.insert("user_message_id".to_string(), user_message.id.clone());
|
|
||||||
|
|
||||||
self.finalize_result_and_schedule_compaction(
|
self.finalize_result_and_schedule_compaction(
|
||||||
request.session.clone(),
|
request.session.clone(),
|
||||||
|
|||||||
@ -200,7 +200,7 @@ impl BusToolCallEmitter {
|
|||||||
priority: "medium".to_string(),
|
priority: "medium".to_string(),
|
||||||
created_at: now + idx as i64,
|
created_at: now + idx as i64,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
created_by_message_id: message.tool_call_id.clone(),
|
created_by_message_id: Some(message.id.clone()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -21,7 +21,6 @@ use crate::command::handlers::load_chat_messages::LoadChatMessagesCommandHandler
|
|||||||
use crate::command::handlers::load_task_messages::LoadTaskMessagesCommandHandler;
|
use crate::command::handlers::load_task_messages::LoadTaskMessagesCommandHandler;
|
||||||
use crate::command::handlers::load_topic::LoadTopicCommandHandler;
|
use crate::command::handlers::load_topic::LoadTopicCommandHandler;
|
||||||
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
use crate::command::handlers::save_session::SaveSessionCommandHandler;
|
||||||
use crate::command::handlers::save_topic::SaveTopicCommandHandler;
|
|
||||||
use crate::command::handlers::session::SessionCommandHandler;
|
use crate::command::handlers::session::SessionCommandHandler;
|
||||||
use crate::command::handlers::stop_execution::StopExecutionCommandHandler;
|
use crate::command::handlers::stop_execution::StopExecutionCommandHandler;
|
||||||
use crate::command::handlers::switch_topic::SwitchTopicCommandHandler;
|
use crate::command::handlers::switch_topic::SwitchTopicCommandHandler;
|
||||||
@ -412,12 +411,6 @@ async fn handle_inbound(
|
|||||||
state.task_repository.clone(),
|
state.task_repository.clone(),
|
||||||
system_prompt_provider.clone(),
|
system_prompt_provider.clone(),
|
||||||
)));
|
)));
|
||||||
// 注册 save_topic 处理器
|
|
||||||
router.register(Box::new(SaveTopicCommandHandler::new(
|
|
||||||
store.clone(),
|
|
||||||
state.task_repository.clone(),
|
|
||||||
system_prompt_provider.clone(),
|
|
||||||
).with_session_manager(state.session_manager.clone())));
|
|
||||||
// 注册 delete_topic 处理器
|
// 注册 delete_topic 处理器
|
||||||
router.register(Box::new(
|
router.register(Box::new(
|
||||||
DeleteTopicCommandHandler::new(store.clone())
|
DeleteTopicCommandHandler::new(store.clone())
|
||||||
@ -864,7 +857,6 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
|
|||||||
topic_id: None,
|
topic_id: None,
|
||||||
timestamp: Some(msg.timestamp / 1000),
|
timestamp: Some(msg.timestamp / 1000),
|
||||||
reasoning_content: msg.reasoning_content.clone(),
|
reasoning_content: msg.reasoning_content.clone(),
|
||||||
user_message_id: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// AssistantResponse 已携带 reasoning 时,ToolCall 不再重复
|
// AssistantResponse 已携带 reasoning 时,ToolCall 不再重复
|
||||||
@ -881,7 +873,6 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
|
|||||||
topic_id: None,
|
topic_id: None,
|
||||||
timestamp: Some(msg.timestamp / 1000),
|
timestamp: Some(msg.timestamp / 1000),
|
||||||
reasoning_content: tc_reasoning.clone(),
|
reasoning_content: tc_reasoning.clone(),
|
||||||
user_message_id: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
outbound
|
outbound
|
||||||
@ -896,7 +887,6 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
|
|||||||
topic_id: None,
|
topic_id: None,
|
||||||
timestamp: Some(msg.timestamp / 1000),
|
timestamp: Some(msg.timestamp / 1000),
|
||||||
reasoning_content: msg.reasoning_content.clone(),
|
reasoning_content: msg.reasoning_content.clone(),
|
||||||
user_message_id: None,
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -936,7 +926,6 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound>
|
|||||||
topic_id: None,
|
topic_id: None,
|
||||||
timestamp: Some(msg.timestamp / 1000),
|
timestamp: Some(msg.timestamp / 1000),
|
||||||
reasoning_content: None,
|
reasoning_content: None,
|
||||||
user_message_id: None,
|
|
||||||
}],
|
}],
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,8 +150,6 @@ pub enum WsOutbound {
|
|||||||
timestamp: Option<i64>,
|
timestamp: Option<i64>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
reasoning_content: Option<String>,
|
reasoning_content: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
user_message_id: Option<String>,
|
|
||||||
},
|
},
|
||||||
#[serde(rename = "tool_call")]
|
#[serde(rename = "tool_call")]
|
||||||
ToolCall {
|
ToolCall {
|
||||||
@ -169,8 +167,6 @@ pub enum WsOutbound {
|
|||||||
timestamp: Option<i64>,
|
timestamp: Option<i64>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
reasoning_content: Option<String>,
|
reasoning_content: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
user_message_id: Option<String>,
|
|
||||||
},
|
},
|
||||||
#[serde(rename = "tool_result")]
|
#[serde(rename = "tool_result")]
|
||||||
ToolResult {
|
ToolResult {
|
||||||
@ -284,8 +280,6 @@ 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")]
|
||||||
topic_id: Option<String>,
|
topic_id: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
user_message_id: Option<String>,
|
|
||||||
},
|
},
|
||||||
#[serde(rename = "stream_end")]
|
#[serde(rename = "stream_end")]
|
||||||
StreamEnd {
|
StreamEnd {
|
||||||
|
|||||||
@ -112,7 +112,6 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
topic_id: message.metadata.get("topic_id").cloned(),
|
topic_id: message.metadata.get("topic_id").cloned(),
|
||||||
timestamp: Some(crate::protocol::now_timestamp()),
|
timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: message.reasoning_content.clone(),
|
reasoning_content: message.reasoning_content.clone(),
|
||||||
user_message_id: message.metadata.get("user_message_id").cloned(),
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall {
|
OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall {
|
||||||
@ -132,7 +131,6 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
topic_id: message.metadata.get("topic_id").cloned(),
|
topic_id: message.metadata.get("topic_id").cloned(),
|
||||||
timestamp: Some(crate::protocol::now_timestamp()),
|
timestamp: Some(crate::protocol::now_timestamp()),
|
||||||
reasoning_content: message.reasoning_content.clone(),
|
reasoning_content: message.reasoning_content.clone(),
|
||||||
user_message_id: message.metadata.get("user_message_id").cloned(),
|
|
||||||
}],
|
}],
|
||||||
OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult {
|
OutboundEventKind::ToolResult => vec![WsOutbound::ToolResult {
|
||||||
id: message
|
id: message
|
||||||
@ -184,7 +182,6 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
reasoning_delta: message.reasoning_content.clone(),
|
reasoning_delta: message.reasoning_content.clone(),
|
||||||
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
|
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
|
||||||
topic_id: message.metadata.get("topic_id").cloned(),
|
topic_id: message.metadata.get("topic_id").cloned(),
|
||||||
user_message_id: message.metadata.get("user_message_id").cloned(),
|
|
||||||
}],
|
}],
|
||||||
OutboundEventKind::StreamEnd => vec![WsOutbound::StreamEnd {
|
OutboundEventKind::StreamEnd => vec![WsOutbound::StreamEnd {
|
||||||
id: message.tool_call_id.clone().unwrap_or_default(),
|
id: message.tool_call_id.clone().unwrap_or_default(),
|
||||||
|
|||||||
@ -235,7 +235,7 @@ impl SubAgentEmitter {
|
|||||||
priority: "medium".to_string(),
|
priority: "medium".to_string(),
|
||||||
created_at: now + idx as i64,
|
created_at: now + idx as i64,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
created_by_message_id: message.tool_call_id.clone(),
|
created_by_message_id: Some(message.id.clone()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -401,12 +401,11 @@ function App() {
|
|||||||
|
|
||||||
// 点击待办项后滚动到对应消息
|
// 点击待办项后滚动到对应消息
|
||||||
const handleTodoClick = useCallback((todo: TodoItemSummary) => {
|
const handleTodoClick = useCallback((todo: TodoItemSummary) => {
|
||||||
|
// 直接使用后端返回的 created_by_message_id
|
||||||
if (todo.created_by_message_id) {
|
if (todo.created_by_message_id) {
|
||||||
// 先清再设,确保同一 todo 重复点击也能触发 useEffect
|
setHighlightedMessageId(todo.created_by_message_id)
|
||||||
setHighlightedMessageId(null)
|
|
||||||
const msgId = todo.created_by_message_id
|
|
||||||
setTimeout(() => setHighlightedMessageId(msgId), 0)
|
|
||||||
} else {
|
} else {
|
||||||
|
// 如果消息 ID 不存在(旧数据),给出友好提示
|
||||||
alert('该待办的完成记录无法定位,可能是历史数据')
|
alert('该待办的完成记录无法定位,可能是历史数据')
|
||||||
}
|
}
|
||||||
}, [setHighlightedMessageId])
|
}, [setHighlightedMessageId])
|
||||||
@ -486,12 +485,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤无实质内容的 merged_tool(无结果且非等待中)
|
return result
|
||||||
return result.filter(msg => {
|
|
||||||
if (msg.type !== 'merged_tool') return true
|
|
||||||
if (msg.status === 'pending') return true
|
|
||||||
return !!(msg.resultContent && msg.resultContent.trim())
|
|
||||||
})
|
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
// 视图标识:用于 MessageList 保存/恢复每个视图的滚动位置
|
// 视图标识:用于 MessageList 保存/恢复每个视图的滚动位置
|
||||||
|
|||||||
@ -606,9 +606,8 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 隐藏无可见内容的助手消息(无文本,且无思考或思考被关闭)
|
// 隐藏思考且无实质内容时,不渲染空的助手消息气泡
|
||||||
const hasVisibleContent = !!(message.content && message.content.trim()) || (showThinking && message.reasoningContent)
|
if (!isUser && !isTool && !isMergedTool && !showThinking && !message.content.trim() && message.reasoningContent) {
|
||||||
if (!isUser && !isTool && !isMergedTool && !hasVisibleContent) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -167,9 +167,6 @@ export function useChat(): UseChatReturn {
|
|||||||
const [channels, setChannels] = useState<Channel[]>([])
|
const [channels, setChannels] = useState<Channel[]>([])
|
||||||
const [selectedChannel, setSelectedChannel] = useState<string>('websocket')
|
const [selectedChannel, setSelectedChannel] = useState<string>('websocket')
|
||||||
|
|
||||||
// Track user message IDs already synced from backend to avoid duplicate updates
|
|
||||||
const syncedUserMessageIdsRef = useRef<Set<string>>(new Set())
|
|
||||||
|
|
||||||
// Message ID generator
|
// Message ID generator
|
||||||
const messageIdCounter = useRef(0)
|
const messageIdCounter = useRef(0)
|
||||||
const generateMessageId = () => {
|
const generateMessageId = () => {
|
||||||
@ -359,23 +356,6 @@ export function useChat(): UseChatReturn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync backend user message ID to the last local user message,
|
|
||||||
// so that created_by_message_id (backend UUID) can match DOM data-message-id
|
|
||||||
const applyUserMessageId = useCallback((userMessageId: string) => {
|
|
||||||
if (syncedUserMessageIdsRef.current.has(userMessageId)) return
|
|
||||||
syncedUserMessageIdsRef.current.add(userMessageId)
|
|
||||||
setMessages(prev => {
|
|
||||||
for (let i = prev.length - 1; i >= 0; i--) {
|
|
||||||
if (prev[i].role === 'user') {
|
|
||||||
const updated = [...prev]
|
|
||||||
updated[i] = { ...updated[i], id: userMessageId }
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prev
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleServerMessage = useCallback((message: WsOutbound) => {
|
const handleServerMessage = useCallback((message: WsOutbound) => {
|
||||||
console.log('Received message:', message)
|
console.log('Received message:', message)
|
||||||
|
|
||||||
@ -659,7 +639,6 @@ export function useChat(): UseChatReturn {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
if (msg.user_message_id) applyUserMessageId(msg.user_message_id)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -699,7 +678,6 @@ export function useChat(): UseChatReturn {
|
|||||||
if (currentTopic && !currentTopic.description) {
|
if (currentTopic && !currentTopic.description) {
|
||||||
setTopicRefreshTrigger(n => n + 1)
|
setTopicRefreshTrigger(n => n + 1)
|
||||||
}
|
}
|
||||||
if (msg.user_message_id) applyUserMessageId(msg.user_message_id)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -722,7 +700,6 @@ export function useChat(): UseChatReturn {
|
|||||||
reasoningContent: msg.reasoning_content,
|
reasoningContent: msg.reasoning_content,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
if (msg.user_message_id) applyUserMessageId(msg.user_message_id)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,6 @@ export interface AssistantResponse {
|
|||||||
topic_id?: string
|
topic_id?: string
|
||||||
timestamp?: number
|
timestamp?: number
|
||||||
reasoning_content?: string
|
reasoning_content?: string
|
||||||
user_message_id?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolCall {
|
export interface ToolCall {
|
||||||
@ -61,7 +60,6 @@ export interface ToolCall {
|
|||||||
topic_id?: string
|
topic_id?: string
|
||||||
timestamp?: number
|
timestamp?: number
|
||||||
reasoning_content?: string
|
reasoning_content?: string
|
||||||
user_message_id?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolResult {
|
export interface ToolResult {
|
||||||
@ -268,7 +266,6 @@ export interface StreamDelta {
|
|||||||
reasoning_delta?: string
|
reasoning_delta?: string
|
||||||
subagent_task_id?: string
|
subagent_task_id?: string
|
||||||
topic_id?: string
|
topic_id?: string
|
||||||
user_message_id?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamEnd {
|
export interface StreamEnd {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user