- Auto-connect WebSocket on page load in service console - Settings modal for editing sgclaw_config.json (API key, base URL, model, skills dir, etc.) - UpdateConfig/ConfigUpdated protocol messages for remote config save - save_to_path() for SgClawSettings serialization - ConfigUpdated handler in sg_claw_client binary - Protocol serialization tests for new message types - HTML test assertions for auto-connect and settings UI - Additional pending changes: deterministic submit, org units, lineloss xlsx export, browser script tool, and docs 🤖 Generated with [Qoder][https://qoder.com]
106 lines
3.6 KiB
Rust
106 lines
3.6 KiB
Rust
use std::io::{self, BufRead};
|
|
|
|
use sgclaw::service::{ClientMessage, ServiceMessage};
|
|
use tungstenite::{connect, Message};
|
|
|
|
fn main() -> std::process::ExitCode {
|
|
match run() {
|
|
Ok(()) => std::process::ExitCode::SUCCESS,
|
|
Err(err) => {
|
|
eprintln!("sg_claw_client failed: {err}");
|
|
std::process::ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_request(input: &str) -> (ClientMessage, bool) {
|
|
match input.trim() {
|
|
"/connect" => (ClientMessage::Connect, true),
|
|
"/start" => (ClientMessage::Start, true),
|
|
"/stop" => (ClientMessage::Stop, true),
|
|
instruction => (
|
|
ClientMessage::SubmitTask {
|
|
instruction: instruction.to_string(),
|
|
conversation_id: String::new(),
|
|
messages: vec![],
|
|
page_url: String::new(),
|
|
page_title: String::new(),
|
|
},
|
|
false,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn run() -> Result<(), String> {
|
|
let service_url = std::env::var("SG_CLAW_SERVICE_WS_URL")
|
|
.unwrap_or_else(|_| "ws://127.0.0.1:42321".to_string());
|
|
let (mut socket, _) = connect(service_url.as_str()).map_err(|err| err.to_string())?;
|
|
|
|
let stdin = io::stdin();
|
|
|
|
loop {
|
|
eprint!("> ");
|
|
let mut input = String::new();
|
|
let bytes_read = stdin
|
|
.lock()
|
|
.read_line(&mut input)
|
|
.map_err(|err| err.to_string())?;
|
|
if bytes_read == 0 {
|
|
break; // EOF — graceful exit
|
|
}
|
|
if input.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let (request, exit_on_status) = parse_request(&input);
|
|
|
|
let payload = serde_json::to_string(&request).map_err(|err| err.to_string())?;
|
|
socket
|
|
.send(Message::Text(payload.into()))
|
|
.map_err(|err| err.to_string())?;
|
|
|
|
// Inner loop: consume service messages until the task finishes.
|
|
loop {
|
|
match socket.read().map_err(|err| err.to_string())? {
|
|
Message::Text(text) => {
|
|
let message: ServiceMessage =
|
|
serde_json::from_str(&text).map_err(|err| err.to_string())?;
|
|
match message {
|
|
ServiceMessage::StatusChanged { state } => {
|
|
println!("status: {state}");
|
|
if exit_on_status {
|
|
break;
|
|
}
|
|
}
|
|
ServiceMessage::LogEntry { level: _, message } => {
|
|
println!("{message}");
|
|
}
|
|
ServiceMessage::TaskComplete { success: _, summary } => {
|
|
println!("{summary}");
|
|
break;
|
|
}
|
|
ServiceMessage::Busy { message } => {
|
|
eprintln!("busy: {message}");
|
|
break;
|
|
}
|
|
ServiceMessage::Pong => {}
|
|
ServiceMessage::ConfigUpdated { success, message } => {
|
|
if success {
|
|
println!("config updated: {message}");
|
|
} else {
|
|
eprintln!("config update failed: {message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Message::Close(_) => {
|
|
return Err("service disconnected".to_string());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|