mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(vom): update vom algorithm, fix overlay problem with background state machine
This commit is contained in:
@@ -78,6 +78,8 @@ fn main() {
|
||||
|
||||
dump!(SnapshotParams, "tool_snapshot_params");
|
||||
dump!(SnapshotResult, "tool_snapshot_result");
|
||||
dump!(ObserveParams, "tool_observe_params");
|
||||
dump!(ObserveResult, "tool_observe_result");
|
||||
dump!(GetHtmlParams, "tool_get_html_params");
|
||||
dump!(GetHtmlResult, "tool_get_html_result");
|
||||
dump!(ScreenshotParams, "tool_screenshot_params");
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Observable browser-side effect class for a protocol method.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MethodEffect {
|
||||
/// Reads browser/session state without dispatching page input.
|
||||
PassiveRead,
|
||||
/// Dispatches temporary input such as hover probes. It must not submit,
|
||||
/// navigate, or persist page state, but it can trigger page event handlers.
|
||||
TransientInput,
|
||||
/// Drives browser/page state such as clicking, filling, navigation, tabs,
|
||||
/// or arbitrary page script.
|
||||
BrowserMutation,
|
||||
/// Control-plane/session lifecycle operation. These are deliberately not
|
||||
/// gated by the pending browser-action interrupt path.
|
||||
ControlPlane,
|
||||
}
|
||||
|
||||
/// Namespaced method string (`system.handshake`, `tool.tab_list`, …).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Method {
|
||||
@@ -58,6 +74,8 @@ pub enum Method {
|
||||
ToolSelect,
|
||||
#[serde(rename = "tool.snapshot")]
|
||||
ToolSnapshot,
|
||||
#[serde(rename = "tool.observe")]
|
||||
ToolObserve,
|
||||
#[serde(rename = "tool.get_html")]
|
||||
ToolGetHtml,
|
||||
#[serde(rename = "tool.screenshot")]
|
||||
@@ -86,13 +104,13 @@ pub enum Method {
|
||||
}
|
||||
|
||||
impl Method {
|
||||
/// Whether this RPC may modify browser state.
|
||||
/// Browser-side effect classification for this method.
|
||||
///
|
||||
/// Used by the daemon's pending-interrupt machinery: when the
|
||||
/// user has clicked the agent-window mask's stop button, the
|
||||
/// next *mutating* tool call for that session is rejected with
|
||||
/// `ErrorCode::UserAborted`. Read-only tools and session-
|
||||
/// lifecycle RPCs pass through transparently.
|
||||
/// next tool call that dispatches browser/page input for that
|
||||
/// session is rejected with `ErrorCode::UserAborted`. Passive reads
|
||||
/// and control-plane RPCs pass through transparently.
|
||||
///
|
||||
/// **Compile-time enforcement.** The match below is exhaustive
|
||||
/// (no `_ =>` fallthrough). Adding a new `Method` variant is a
|
||||
@@ -101,9 +119,12 @@ impl Method {
|
||||
///
|
||||
/// **Judgment calls** (read these before adding new variants):
|
||||
///
|
||||
/// * `tool.evaluate` is classified as mutating because the
|
||||
/// * `tool.evaluate` is classified as browser-mutating because the
|
||||
/// daemon cannot statically distinguish a `document.title`
|
||||
/// read from a `form.submit()` write.
|
||||
/// * `tool.observe` is classified as transient input: its bounded
|
||||
/// hover probes do not commit browser state, but they dispatch real
|
||||
/// page input events and therefore must be gated like automation.
|
||||
/// * `tool.wait_*` are classified as read-only: they do not
|
||||
/// initiate any browser action; they observe state only.
|
||||
/// * `session.*` and `tool.session_*` are NOT gated. Blocking
|
||||
@@ -111,9 +132,9 @@ impl Method {
|
||||
/// tearing down after observing the user's interrupt.
|
||||
/// * `cancel` is NOT gated. It's a control-plane operation
|
||||
/// (stops another in-flight RPC), not a browser action.
|
||||
pub fn is_mutating(&self) -> bool {
|
||||
pub fn effect(&self) -> MethodEffect {
|
||||
match self {
|
||||
// Mutating tool calls — gated by pending-interrupt.
|
||||
// Browser/page mutations — gated by pending-interrupt.
|
||||
Method::ToolTabCreate
|
||||
| Method::ToolTabClose
|
||||
| Method::ToolTabBorrow
|
||||
@@ -130,10 +151,13 @@ impl Method {
|
||||
| Method::ToolEvaluate
|
||||
// May navigate via optional `url` and changes Agent Window
|
||||
// chrome; gate behind pending-interrupt like other writes.
|
||||
| Method::ToolRecordStart => true,
|
||||
| Method::ToolRecordStart => MethodEffect::BrowserMutation,
|
||||
|
||||
// Read-only tool calls — transparent.
|
||||
//
|
||||
// Transient input — no committed browser action, but still page
|
||||
// input. It must be stopped by pending user interrupts.
|
||||
Method::ToolObserve => MethodEffect::TransientInput,
|
||||
|
||||
// Passive reads — transparent.
|
||||
// `record_stop` / `record_await` observe / finish a recording
|
||||
// without driving new automation gestures, so they stay
|
||||
// ungated (teardown after interrupt must still work).
|
||||
@@ -147,7 +171,7 @@ impl Method {
|
||||
| Method::ToolWaitMs
|
||||
| Method::ToolRequestHelp
|
||||
| Method::ToolRecordStop
|
||||
| Method::ToolRecordAwait => false,
|
||||
| Method::ToolRecordAwait => MethodEffect::PassiveRead,
|
||||
|
||||
// Session lifecycle — not gated.
|
||||
Method::SessionStart
|
||||
@@ -155,16 +179,29 @@ impl Method {
|
||||
| Method::SessionStopAll
|
||||
| Method::SessionList
|
||||
| Method::ToolSessionStart
|
||||
| Method::ToolSessionStop => false,
|
||||
| Method::ToolSessionStop => MethodEffect::ControlPlane,
|
||||
|
||||
// System / control — not gated.
|
||||
Method::SystemHandshake
|
||||
| Method::SystemPing
|
||||
| Method::SystemStatus
|
||||
| Method::BrowserList
|
||||
| Method::Cancel => false,
|
||||
| Method::Cancel => MethodEffect::ControlPlane,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this RPC drives browser/page state in the traditional sense.
|
||||
pub fn is_mutating(&self) -> bool {
|
||||
self.effect() == MethodEffect::BrowserMutation
|
||||
}
|
||||
|
||||
/// Whether a pending user interrupt should reject this method.
|
||||
pub fn requires_interrupt_gate(&self) -> bool {
|
||||
matches!(
|
||||
self.effect(),
|
||||
MethodEffect::TransientInput | MethodEffect::BrowserMutation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -209,6 +246,7 @@ mod tests {
|
||||
fn is_mutating_classifies_read_only_tools_as_non_mutating() {
|
||||
assert!(!Method::ToolTabList.is_mutating());
|
||||
assert!(!Method::ToolSnapshot.is_mutating());
|
||||
assert!(!Method::ToolObserve.is_mutating());
|
||||
assert!(!Method::ToolGetHtml.is_mutating());
|
||||
assert!(!Method::ToolScreenshot.is_mutating());
|
||||
assert!(!Method::ToolConsole.is_mutating());
|
||||
@@ -264,4 +302,20 @@ mod tests {
|
||||
assert!(!Method::BrowserList.is_mutating());
|
||||
assert!(!Method::Cancel.is_mutating());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effect_classifies_observe_as_transient_input() {
|
||||
assert_eq!(Method::ToolSnapshot.effect(), MethodEffect::PassiveRead);
|
||||
assert_eq!(Method::ToolObserve.effect(), MethodEffect::TransientInput);
|
||||
assert_eq!(Method::ToolClick.effect(), MethodEffect::BrowserMutation);
|
||||
assert_eq!(Method::Cancel.effect(), MethodEffect::ControlPlane);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_gate_includes_transient_input() {
|
||||
assert!(!Method::ToolSnapshot.requires_interrupt_gate());
|
||||
assert!(Method::ToolObserve.requires_interrupt_gate());
|
||||
assert!(Method::ToolClick.requires_interrupt_gate());
|
||||
assert!(!Method::Cancel.requires_interrupt_gate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Read-only observation tools (`tool.snapshot`, `tool.get_html`, `tool.screenshot`).
|
||||
//! Observation tools (`tool.snapshot`, `tool.observe`, `tool.get_html`, `tool.screenshot`).
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,6 +50,50 @@ pub struct SnapshotResult {
|
||||
pub dialogs: Vec<JavaScriptDialogInfo>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// observe
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parameters for `tool.observe` — produce a semantic VOM observation
|
||||
/// for one tab plus a fresh `@e<N>` ref-store on the extension side.
|
||||
///
|
||||
/// Unlike `tool.snapshot`, this path may run bounded perception probes
|
||||
/// such as hover-surface discovery. It must not submit input, click,
|
||||
/// navigate, or otherwise commit page state.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ObserveParams {
|
||||
pub session_id: String,
|
||||
/// Optional target tab. Defaults to the Agent Window's currently
|
||||
/// active tab.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tab_id: Option<i64>,
|
||||
/// Cap the depth of the rendered VOM tree.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_depth: Option<u32>,
|
||||
/// Soft cap on rendered tokens (approximate; extension uses a
|
||||
/// best-effort heuristic based on character count).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ObserveResult {
|
||||
/// Semantic VOM observation text. Refs are rendered as `@e<N>` so
|
||||
/// the agent can copy them into subsequent interaction tools.
|
||||
pub text: String,
|
||||
/// Number of `@e<N>` refs registered for this session by this
|
||||
/// observation. Equivalent to the size of the new ref-store map.
|
||||
pub ref_count: u32,
|
||||
/// Tab the observation was computed for.
|
||||
pub tab_id: i64,
|
||||
/// Whether the rendered tree was truncated because of `max_depth` /
|
||||
/// `max_tokens`. Agents may re-run with looser caps.
|
||||
#[serde(default)]
|
||||
pub truncated: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub dialogs: Vec<JavaScriptDialogInfo>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// get_html
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -227,4 +271,19 @@ mod tests {
|
||||
let round: ScreenshotResult = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observe_result_round_trips_with_ref_count() {
|
||||
let r = ObserveResult {
|
||||
text: "@vom 1\n @e1 button \"submit\"\n".into(),
|
||||
ref_count: 1,
|
||||
tab_id: 42,
|
||||
truncated: false,
|
||||
dialogs: Vec::new(),
|
||||
};
|
||||
let v = serde_json::to_value(&r).unwrap();
|
||||
assert_eq!(v.get("ref_count").and_then(|v| v.as_u64()), Some(1));
|
||||
let round: ObserveResult = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(round, r);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user