feat: add websocket browser service runtime

Wire the service/browser runtime onto the websocket-driven execution path and add the new browser/service modules needed for the submit flow and runtime integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-04 23:42:27 +08:00
parent 2ae71fb1c9
commit 3e18350320
33 changed files with 4993 additions and 327 deletions

10
src/bin/sg_claw.rs Normal file
View File

@@ -0,0 +1,10 @@
use std::process::ExitCode;
fn main() -> ExitCode {
if let Err(err) = sgclaw::service::run() {
eprintln!("sg_claw failed: {err}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}

78
src/bin/sg_claw_client.rs Normal file
View File

@@ -0,0 +1,78 @@
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 mut input = String::new();
io::stdin()
.lock()
.read_line(&mut input)
.map_err(|err| err.to_string())?;
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())?;
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 {
return Ok(());
}
}
ServiceMessage::LogEntry { level: _, message } => {
println!("{message}");
}
ServiceMessage::TaskComplete { success: _, summary } => {
println!("{summary}");
return Ok(());
}
ServiceMessage::Busy { message } => return Err(message),
}
}
Message::Close(_) => return Err("service disconnected before task completion".to_string()),
_ => {}
}
}
}

View File

@@ -0,0 +1,70 @@
use std::env;
use std::process::ExitCode;
use std::time::Duration;
use sgclaw::{parse_probe_args, run_probe_script, ProbeOutcome};
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("sgbrowser_ws_probe failed: {err}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), String> {
let args: Vec<String> = env::args().skip(1).collect();
let config = match parse_probe_args(&args) {
Ok(config) => config,
Err(err) => return Err(err.to_string()),
};
let results = match run_probe_script(
&config.ws_url,
Duration::from_millis(config.timeout_ms),
config.steps,
) {
Ok(results) => results,
Err(err) => return Err(err.to_string()),
};
for (index, result) in results.iter().enumerate() {
println!("STEP {} {}", index + 1, result.label);
println!("SEND: {}", result.sent);
match &result.outcome {
ProbeOutcome::Received(frames) => {
if frames.is_empty() {
println!("RECV: <none>");
} else {
for frame in frames {
println!("RECV: {}", frame);
}
}
println!("OUTCOME: received");
}
ProbeOutcome::NoReplyExpected => {
println!("RECV: <none>");
println!("OUTCOME: no-reply-expected");
}
ProbeOutcome::TimedOut => {
println!("RECV: <none>");
println!("OUTCOME: timeout");
}
ProbeOutcome::Closed => {
println!("RECV: <none>");
println!("OUTCOME: closed");
}
ProbeOutcome::ConnectFailed(message) => {
println!("RECV: <none>");
println!("OUTCOME: connect-failed");
println!("DETAIL: {}", message);
}
}
if index + 1 < results.len() {
println!();
}
}
Ok(())
}