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(¶ms)?;
18 if method == "server.hello" {
19 return hello(params);
20 }
21 if method == "server.shutdown" {
22 validate_params(method, ¶ms)?;
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, ¶ms)?;
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(¶ms.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(¶ms)?;
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}