Files
claw/src/bin/sg_claw_client.rs
木炎 c60cd308ca feat: service console auto-connect, settings panel, and batch of enhancements
- 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]
2026-04-14 14:32:46 +08:00

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(())
}