pub use crate::protocol::{WsInbound, WsOutbound, serialize_inbound, serialize_outbound}; use crate::command::adapter::InputAdapter; use crate::command::adapters::cli::CliInputAdapter; use crate::command::context::AdapterContext; use futures_util::{SinkExt, StreamExt}; use tokio_tungstenite::{connect_async, tungstenite::Message}; use crate::cli::{InputCommand, InputEvent, InputHandler}; fn parse_message(raw: &str) -> Result { serde_json::from_str(raw) } fn format_json(value: &serde_json::Value) -> String { serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()) } pub async fn run(gateway_url: &str) -> Result<(), Box> { let (ws_stream, _) = connect_async(gateway_url).await?; tracing::info!(url = %gateway_url, "Connected to gateway"); let (mut sender, mut receiver) = ws_stream.split(); let mut input = InputHandler::new(); let mut current_session_id: Option = None; input.write_output("picobot CLI - Commands: /new [title], /save [filepath], /quit\n").await?; // Main loop: poll both stdin and WebSocket loop { tokio::select! { // Handle WebSocket messages msg = receiver.next() => { match msg { Some(Ok(Message::Text(text))) => { let text = text.to_string(); if let Ok(outbound) = parse_message(&text) { match outbound { WsOutbound::AssistantResponse { content, .. } => { input.write_response(&content).await?; } WsOutbound::ToolCall { tool_name, arguments, .. } => { input.write_output(&format!("Tool call: {}\n{}\n", tool_name, format_json(&arguments))).await?; } WsOutbound::ToolResult { tool_name, content, .. } => { input.write_output(&format!("Tool result: {}\n{}\n", tool_name, content)).await?; } WsOutbound::ToolPending { tool_name, content, resume_hint, .. } => { input.write_output(&format!("Tool pending: {}\n{}\n{}\n", tool_name, content, resume_hint)).await?; } WsOutbound::Error { message, .. } => { input.write_output(&format!("Error: {}", message)).await?; } WsOutbound::SessionEstablished { session_id } => { current_session_id = Some(session_id.clone()); #[cfg(debug_assertions)] tracing::debug!(session_id = %session_id, "Session established"); input.write_output(&format!("Session: {}\n", session_id)).await?; } WsOutbound::SessionCreated { session_id, title } => { current_session_id = Some(session_id.clone()); input.write_output(&format!("Created session: {} ({})\n", session_id, title)).await?; } WsOutbound::SessionSaved { session_id, filepath } => { input.write_output(&format!("Saved session {} to: {}\n", session_id, filepath)).await?; } _ => {} } } } Some(Ok(Message::Close(_))) | None => { tracing::info!("Gateway disconnected"); input.write_output("Gateway disconnected").await?; break; } _ => {} } } // Handle stdin input result = input.read_input("> ") => { match result { Ok(Some(event)) => { match event { InputEvent::Command(InputCommand::Exit) => { input.write_output("Goodbye!").await?; break; } InputEvent::Command(InputCommand::New(title)) => { // 使用 CliInputAdapter 构建 Command let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("cli") .with_session_id(current_session_id.as_deref().unwrap_or("")); // 构建输入字符串 let input_str = match title { Some(t) => format!("/new {}", t), None => "/new".to_string(), }; // 解析为 Command match adapter.try_parse(&input_str, ctx) { Ok(Some(command)) => { // 序列化为 JSON let json = serde_json::to_string(&command).unwrap_or_default(); // 通过 Command 消息发送 let inbound = WsInbound::Command { payload: json }; if let Ok(text) = serialize_inbound(&inbound) { let _ = sender.send(Message::Text(text.into())).await; } } Ok(None) => { tracing::warn!("Failed to parse /new command"); } Err(e) => { tracing::error!(error = %e, "Error parsing /new command"); } } continue; } InputEvent::Command(InputCommand::Save(filepath)) => { // 使用 CliInputAdapter 构建 Command let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("cli") .with_session_id(current_session_id.as_deref().unwrap_or("")); // 构建输入字符串 let input_str = match filepath { Some(p) => format!("/save {}", p), None => "/save".to_string(), }; // 解析为 Command match adapter.try_parse(&input_str, ctx) { Ok(Some(command)) => { // 序列化为 JSON let json = serde_json::to_string(&command).unwrap_or_default(); // 通过 Command 消息发送 let inbound = WsInbound::Command { payload: json }; if let Ok(text) = serialize_inbound(&inbound) { let _ = sender.send(Message::Text(text.into())).await; } } Ok(None) => { tracing::warn!("Failed to parse /save command"); } Err(e) => { tracing::error!(error = %e, "Error parsing /save command"); } } continue; } InputEvent::Command(InputCommand::Sessions) => { // 使用 CliInputAdapter 构建 Command let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("cli") .with_session_id(current_session_id.as_deref().unwrap_or("")); // 解析为 Command match adapter.try_parse("/list", ctx) { Ok(Some(command)) => { // 序列化为 JSON let json = serde_json::to_string(&command).unwrap_or_default(); // 通过 Command 消息发送 let inbound = WsInbound::Command { payload: json }; if let Ok(text) = serialize_inbound(&inbound) { let _ = sender.send(Message::Text(text.into())).await; } } Ok(None) => { tracing::warn!("Failed to parse /list command"); } Err(e) => { tracing::error!(error = %e, "Error parsing /list command"); } } continue; } InputEvent::Command(InputCommand::Use(session_id)) => { // 使用 CliInputAdapter 构建 Command let adapter = CliInputAdapter::new(); let ctx = AdapterContext::new("cli") .with_session_id(current_session_id.as_deref().unwrap_or("")); // 构建输入字符串 let input_str = format!("/use {}", session_id); // 解析为 Command match adapter.try_parse(&input_str, ctx) { Ok(Some(command)) => { // 序列化为 JSON let json = serde_json::to_string(&command).unwrap_or_default(); // 通过 Command 消息发送 let inbound = WsInbound::Command { payload: json }; if let Ok(text) = serialize_inbound(&inbound) { let _ = sender.send(Message::Text(text.into())).await; } // 更新当前会话 ID current_session_id = Some(session_id.clone()); } Ok(None) => { tracing::warn!("Failed to parse /use command"); } Err(e) => { tracing::error!(error = %e, "Error parsing /use command"); } } continue; } InputEvent::Message(msg) => { let inbound = WsInbound::Message { content: msg.content, attachments: Vec::new(), channel: None, chat_id: current_session_id.clone(), sender_id: None, }; if let Ok(text) = serialize_inbound(&inbound) { if sender.send(Message::Text(text.into())).await.is_err() { tracing::error!("Failed to send message to gateway"); break; } } } } } Ok(None) => break, Err(e) => { tracing::error!(error = %e, "Input error"); break; } } } } } Ok(()) }