Skip to main content

eoka_server/
dispatch.rs

1use serde_json::Value;
2
3use crate::methods::{browser, page};
4use crate::state::AppState;
5use eoka_protocol::sidecar::{
6    method_names, validate_params, validate_safe_numbers, AgentObserveParams, AgentStepParams,
7    ErrorCode, HelloParams, HelloResult, ServerError, TransportLimits, CAPABILITY_NATIVE_RECORDING,
8    CAPABILITY_SCREENSHOT_FILE, CAPABILITY_SCREENSHOT_OPTIONS, MAX_IN_FLIGHT, MAX_MESSAGE_BYTES,
9    PROTOCOL_VERSION,
10};
11
12pub async fn dispatch(
13    state: &mut AppState,
14    method: &str,
15    params: Value,
16) -> Result<Value, ServerError> {
17    validate_safe_numbers(&params)?;
18    if method == "server.hello" {
19        return hello(params);
20    }
21    if method == "server.shutdown" {
22        validate_params(method, &params)?;
23        state.shutdown().await?;
24        return Ok(serde_json::json!({}));
25    }
26    if method == "agent.observe" {
27        let args: AgentObserveParams = serde_json::from_value(params)
28            .map_err(|error| ServerError::invalid_params(error.to_string()))?;
29        let browser = state.browser()?.clone();
30        let page = state.page(&args.page_id)?.clone();
31        let result = state
32            .agent
33            .observe(&browser, &page, args.args)
34            .await
35            .map_err(agent_error)?;
36        return serde_json::to_value(result)
37            .map_err(|error| ServerError::internal(error.to_string()));
38    }
39    if method == "agent.step" {
40        let args: AgentStepParams = serde_json::from_value(params)
41            .map_err(|error| ServerError::invalid_params(error.to_string()))?;
42        let browser = state.browser()?.clone();
43        let page = state.page(&args.page_id)?.clone();
44        let result = state
45            .agent
46            .step(&browser, &page, &state.operations, args.args)
47            .await
48            .map_err(agent_error)?;
49        return serde_json::to_value(result)
50            .map_err(|error| ServerError::internal(error.to_string()));
51    }
52    if crate::operations::sidecar::supports(method) {
53        return crate::operations::sidecar::dispatch(state, method, params).await;
54    }
55    if crate::recordings::supports_control(method) {
56        return crate::recordings::control(&state.recordings, method, params).await;
57    }
58    if matches!(method, "browser.tabs" | "browser.close") {
59        validate_params(method, &params)?;
60    }
61    if matches!(method, "browser.close_tab" | "page.close") {
62        state.operations.stop().await;
63    }
64    match method {
65        "browser.launch" => browser::launch(state, params).await,
66        "browser.connect" => browser::connect(state, params).await,
67        "browser.new_page" => browser::new_page(state, params).await,
68        "browser.tabs" => browser::tabs(state, params).await,
69        "browser.close_tab" => browser::close_tab(state, params).await,
70        "browser.close" => browser::close(state, params).await,
71
72        "page.text" => page::text(state, params).await,
73        "page.content" => page::content(state, params).await,
74        "page.title" => page::title(state, params).await,
75        "page.url" => page::url(state, params).await,
76        "page.get_text" => page::get_text(state, params).await,
77        "page.get_attribute" => page::get_attribute(state, params).await,
78        "page.exists" => page::exists(state, params).await,
79        "page.wait_for" => page::wait_for(state, params).await,
80        "page.wait_for_visible" => page::wait_for_visible(state, params).await,
81        "page.wait_for_text" => page::wait_for_text(state, params).await,
82        "page.evaluate" => page::evaluate(state, params).await,
83        "page.execute" => page::execute(state, params).await,
84        "page.fetch" => page::fetch(state, params).await,
85        "page.fetch_bounded" => page::fetch_bounded(state, params).await,
86        "page.capture_state" => page::capture_state(state, params).await,
87        "page.restore_state" => page::restore_state(state, params).await,
88        "page.screenshot" => page::screenshot(state, params).await,
89        "page.screenshot_to_file" => page::screenshot_to_file(state, params).await,
90        "recording.start" => {
91            let params: eoka_protocol::sidecar::RecordingStartParams =
92                serde_json::from_value(params)
93                    .map_err(|error| ServerError::invalid_params(error.to_string()))?;
94            let page = state.page(&params.page_id).cloned();
95            let record = state.recordings.start(params, page).await?;
96            serde_json::to_value(record).map_err(|error| ServerError::internal(error.to_string()))
97        }
98        "page.set_viewport" => page::set_viewport(state, params).await,
99        "page.reset_viewport" => page::reset_viewport(state, params).await,
100        "page.mouse_down" => page::mouse_down(state, params).await,
101        "page.mouse_move" => page::mouse_move(state, params).await,
102        "page.mouse_up" => page::mouse_up(state, params).await,
103        "page.key_down" => page::key_down(state, params).await,
104        "page.key_up" => page::key_up(state, params).await,
105        "page.release_all_inputs" => page::release_all_inputs(state, params).await,
106        "page.solve_captcha" => page::solve_captcha(state, params).await,
107        "page.close" => page::close(state, params).await,
108
109        _ => Err(ServerError::unknown_method(method)),
110    }
111}
112
113pub fn hello(params: Value) -> Result<Value, ServerError> {
114    validate_safe_numbers(&params)?;
115    let params: HelloParams = serde_json::from_value(params)
116        .map_err(|error| ServerError::invalid_params(error.to_string()))?;
117    if params.protocol.major != PROTOCOL_VERSION.major
118        || params.protocol.minor > PROTOCOL_VERSION.minor
119    {
120        return Err(ServerError::new(
121            ErrorCode::ProtocolMismatch,
122            format!(
123                "unsupported protocol {}.{}; server supports {}.{}",
124                params.protocol.major,
125                params.protocol.minor,
126                PROTOCOL_VERSION.major,
127                PROTOCOL_VERSION.minor
128            ),
129        ));
130    }
131    serde_json::to_value(HelloResult {
132        protocol: PROTOCOL_VERSION,
133        server_version: env!("CARGO_PKG_VERSION").to_owned(),
134        core_version: env!("EOKA_CORE_VERSION").to_owned(),
135        methods: method_names().into_iter().map(str::to_owned).collect(),
136        capabilities: vec![
137            "durableActions".to_owned(),
138            "operationCancellation".to_owned(),
139            "gracefulShutdown".to_owned(),
140            "explicitExecutablePath".to_owned(),
141            "browserAttachment".to_owned(),
142            "runtimeIsolation".to_owned(),
143            "sandboxControl".to_owned(),
144            "viewportEmulation".to_owned(),
145            "boundedFetch".to_owned(),
146            CAPABILITY_SCREENSHOT_OPTIONS.to_owned(),
147            CAPABILITY_SCREENSHOT_FILE.to_owned(),
148            CAPABILITY_NATIVE_RECORDING.to_owned(),
149        ],
150        limits: TransportLimits {
151            max_message_bytes: MAX_MESSAGE_BYTES,
152            max_in_flight: MAX_IN_FLIGHT,
153        },
154    })
155    .map_err(|error| ServerError::internal(error.to_string()))
156}
157
158fn agent_error(error: anyhow::Error) -> ServerError {
159    match error.downcast::<eoka::Error>() {
160        Ok(error) => error.into(),
161        Err(error) => ServerError::internal(format!("{error:#}")),
162    }
163}