Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,14 @@ rmcp = { version = "1.7.0", features = [
] }
reqwest = { version = "0.13", features = ["json", "stream", "blocking"] }
futures = "0.3"
tokio-tungstenite = "0.29"
clipboard-rs = "0.3.4"
html5gum = { version = "0.8.3", default-features = false }
sha2 = "0.10"
velopack = { version = "=0.0.1589-ga2c5a97", features = ["public-utils"] }
tempfile = "3"

[dev-dependencies]
tempfile = "3"

[profile.dev]
# dev:本地调试用,不生成 debug symbols,最小化磁盘占用
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"core:window:allow-start-dragging",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-set-title",
"core:window:allow-set-focus",
"core:window:allow-set-size",
"core:window:allow-set-position",
Expand Down
92 changes: 92 additions & 0 deletions apps/desktop/src-tauri/src/commands/browser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use tauri::State;

use crate::core::browser::{
types::{
BrowserActRequest, BrowserActResult, BrowserConnectExistingRequest,
BrowserConnectExistingResult, BrowserExistingSession, BrowserNavigateRequest,
BrowserObservation, BrowserObserveRequest, BrowserStartRequest, BrowserStatus,
BrowserTabRequest,
},
BrowserRuntime,
};

#[tauri::command]
pub fn browser_status(runtime: State<'_, BrowserRuntime>) -> BrowserStatus {
runtime.status()
}

#[tauri::command]
pub async fn browser_start(
runtime: State<'_, BrowserRuntime>,
request: BrowserStartRequest,
) -> Result<BrowserStatus, String> {
runtime.start(request).await
}

#[tauri::command]
pub async fn browser_discover_existing(
runtime: State<'_, BrowserRuntime>,
) -> Result<Vec<BrowserExistingSession>, String> {
runtime.discover_existing_sessions().await
}

#[tauri::command]
pub async fn browser_connect_existing(
runtime: State<'_, BrowserRuntime>,
request: BrowserConnectExistingRequest,
) -> Result<BrowserConnectExistingResult, String> {
runtime.connect_existing(request).await
}

#[tauri::command]
pub fn browser_stop(runtime: State<'_, BrowserRuntime>) -> BrowserStatus {
runtime.stop()
}

#[tauri::command]
pub async fn browser_navigate(
runtime: State<'_, BrowserRuntime>,
request: BrowserNavigateRequest,
) -> Result<BrowserStatus, String> {
runtime.navigate(request).await
}

#[tauri::command]
pub async fn browser_back(
runtime: State<'_, BrowserRuntime>,
request: BrowserTabRequest,
) -> Result<BrowserStatus, String> {
runtime.history_action(request, "back").await
}

#[tauri::command]
pub async fn browser_forward(
runtime: State<'_, BrowserRuntime>,
request: BrowserTabRequest,
) -> Result<BrowserStatus, String> {
runtime.history_action(request, "forward").await
}

#[tauri::command]
pub async fn browser_reload(
runtime: State<'_, BrowserRuntime>,
request: BrowserTabRequest,
) -> Result<BrowserStatus, String> {
runtime.history_action(request, "reload").await
}

#[tauri::command]
pub async fn browser_observe(
runtime: State<'_, BrowserRuntime>,
request: BrowserObserveRequest,
) -> Result<BrowserObservation, String> {
runtime.observe(request).await
}

#[tauri::command]
pub async fn browser_act(
runtime: State<'_, BrowserRuntime>,
request: BrowserActRequest,
) -> Result<BrowserActResult, String> {
runtime.act(request).await
}
12 changes: 12 additions & 0 deletions apps/desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

//! 命令入口模块。
pub mod autostart;
pub mod browser;
pub mod built_in_tools;
pub mod clipboard;
pub mod database;
Expand Down Expand Up @@ -76,5 +77,16 @@ pub fn invoke_handler<R: tauri::Runtime>(
updater::updater_check_for_updates,
updater::updater_download_update,
updater::updater_install_update,
browser::browser_status,
browser::browser_start,
browser::browser_discover_existing,
browser::browser_connect_existing,
browser::browser_stop,
browser::browser_navigate,
browser::browser_back,
browser::browser_forward,
browser::browser_reload,
browser::browser_observe,
browser::browser_act,
]
}
144 changes: 144 additions & 0 deletions apps/desktop/src-tauri/src/core/browser/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use super::{
endpoint::validate_stale_navigation_token,
types::{BrowserActOperation, BrowserActRequest, BrowserDomRef},
};

const MAX_ACTION_TEXT_BYTES: usize = 16 * 1024;
const MAX_ACTION_KEY_BYTES: usize = 64;
const MAX_FORM_FIELDS: usize = 50;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrowserResolvedFormField {
pub navigation_token: String,
pub selector: String,
pub value: String,
}

#[derive(Debug, Clone)]
pub struct BrowserResolvedAction<'a> {
pub reference: Option<&'a BrowserDomRef>,
pub form_fields: Vec<BrowserResolvedFormField>,
pub page_navigation_token: Option<String>,
pub requires_current_observation: bool,
}

pub fn action_ref_id(request: &BrowserActRequest) -> Option<&str> {
request.ref_id.as_deref().or(request.target_ref.as_deref())
}

pub fn resolve_ref_action<'a>(
request: &BrowserActRequest,
refs: &'a [BrowserDomRef],
) -> Result<BrowserResolvedAction<'a>, String> {
match request.action {
BrowserActOperation::Type => {
let text = request
.text
.as_deref()
.ok_or_else(|| "type requires text".to_string())?;
validate_action_text(text)?;
}
BrowserActOperation::Fill => {
let value = request
.value
.as_deref()
.ok_or_else(|| "fill requires value".to_string())?;
validate_action_text(value)?;
}
BrowserActOperation::PressKey => {
let key = request
.key
.as_deref()
.ok_or_else(|| "press_key requires key".to_string())?;
if key.is_empty() || key.len() > MAX_ACTION_KEY_BYTES {
return Err("press_key key is invalid".to_string());
}
}
_ => {}
}

if matches!(
request.action,
BrowserActOperation::Click | BrowserActOperation::Type | BrowserActOperation::Fill
) && action_ref_id(request).is_none()
{
return Err("Browser click requires an observed ref and navigationToken".to_string());
}

if request.action == BrowserActOperation::FillForm {
let fields = request
.fields
.as_ref()
.ok_or_else(|| "fill_form requires fields".to_string())?;
if fields.len() > MAX_FORM_FIELDS {
return Err("fill_form field count exceeds the size limit".to_string());
}
let mut resolved_fields = Vec::with_capacity(fields.len());
for field in fields {
validate_action_text(&field.value)?;
let reference = find_ref(refs, &field.ref_id)?;
validate_stale_navigation_token(&field.navigation_token, &reference.navigation_token)?;
if !reference.editable {
return Err("Browser target is not editable".to_string());
}
resolved_fields.push(BrowserResolvedFormField {
navigation_token: reference.navigation_token.clone(),
selector: reference.selector.clone(),
value: field.value.clone(),
});
}
return Ok(BrowserResolvedAction {
reference: None,
form_fields: resolved_fields,
page_navigation_token: None,
requires_current_observation: false,
});
}

let Some(ref_id) = action_ref_id(request) else {
let requires_current_observation = matches!(
request.action,
BrowserActOperation::PressKey | BrowserActOperation::Scroll
);
return Ok(BrowserResolvedAction {
reference: None,
form_fields: Vec::new(),
page_navigation_token: request.navigation_token.clone(),
requires_current_observation,
});
};
let reference = find_ref(refs, ref_id)?;
let supplied = request
.navigation_token
.as_deref()
.ok_or_else(|| "Browser action requires navigationToken for ref targets".to_string())?;
validate_stale_navigation_token(supplied, &reference.navigation_token)?;

if matches!(
request.action,
BrowserActOperation::Type | BrowserActOperation::Fill
) && !reference.editable
{
return Err("Browser target is not editable".to_string());
}
Ok(BrowserResolvedAction {
reference: Some(reference),
form_fields: Vec::new(),
page_navigation_token: None,
requires_current_observation: false,
})
}

fn find_ref<'a>(refs: &'a [BrowserDomRef], ref_id: &str) -> Result<&'a BrowserDomRef, String> {
refs.iter()
.find(|candidate| candidate.ref_id == ref_id)
.ok_or_else(|| format!("Browser ref '{ref_id}' was not found; observe again before acting"))
}

fn validate_action_text(value: &str) -> Result<(), String> {
if value.len() > MAX_ACTION_TEXT_BYTES {
Err("Browser action text exceeds the size limit".to_string())
} else {
Ok(())
}
}
Loading