mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat: add bsk network read-only command
Add `bsk network` as a sibling of the `console` command from #7, reading the buffered network responses / failures for a tab. Fills the remaining half of the request in #2 (console landed in #7; network did not). Mirrors the console command's conventions exactly: bsk network --session <id> [--tab-id N] [--since C] [--limit N] [--max-text-chars N] The extension enables `Network` on attach (best-effort) and buffers `responseReceived` / `loadingFailed` events per tab with a monotonic sequence, correlating them to `requestWillBeSent` for method/URL. Reads are cursor-paginated (`since` -> `next_since`) and bounded (`limit`, `max_text_chars`) for agent-context safety, same as console. Entries carry the real HTTP status code (response) or the CDP failure reason (failure) — things `performance.getEntriesByType('resource')` can't give. Classified read-only (pass-through under the pending-interrupt gate); defaults to the Agent Window's active tab. Wires bsk-protocol (Method + params/result/entry structs + schema), bsk-cli (network subcommand + daemon dispatch allow-list) and the extension (handler + dispatcher route), with unit / parse / IPC tests paralleling the console command. Refs #2.
This commit is contained in:
@@ -5,6 +5,7 @@ pub mod dialog;
|
||||
pub mod human_loop;
|
||||
pub mod interaction;
|
||||
pub mod navigation;
|
||||
pub mod network;
|
||||
pub mod observation;
|
||||
pub mod script;
|
||||
pub mod session;
|
||||
@@ -16,6 +17,7 @@ pub use dialog::*;
|
||||
pub use human_loop::*;
|
||||
pub use interaction::*;
|
||||
pub use navigation::*;
|
||||
pub use network::*;
|
||||
pub use observation::*;
|
||||
pub use script::*;
|
||||
pub use session::*;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
//! `tool.network` — read buffered network responses / failures.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NetworkParams {
|
||||
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>,
|
||||
/// Return entries with sequence strictly greater than this cursor.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub since: Option<u64>,
|
||||
/// Maximum number of entries to return. Extension applies safe defaults and caps.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(range(min = 1))]
|
||||
pub limit: Option<u32>,
|
||||
/// Maximum characters returned per URL / error text. Extension applies safe defaults and caps.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(range(min = 1))]
|
||||
pub max_text_chars: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkEntryKind {
|
||||
/// A response was received (`Network.responseReceived`).
|
||||
Response,
|
||||
/// The request failed before completing (`Network.loadingFailed`).
|
||||
Failure,
|
||||
}
|
||||
|
||||
impl NetworkEntryKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Response => "response",
|
||||
Self::Failure => "failure",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NetworkEntry {
|
||||
pub sequence: u64,
|
||||
pub kind: NetworkEntryKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub method: Option<String>,
|
||||
pub url: String,
|
||||
/// HTTP status code (`response` entries only).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub status_text: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub resource_type: Option<String>,
|
||||
/// CDP failure reason (`failure` entries only).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error_text: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub timestamp: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NetworkResult {
|
||||
pub tab_id: i64,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub entries: Vec<NetworkEntry>,
|
||||
pub next_since: u64,
|
||||
#[serde(default)]
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn network_params_omit_optional_fields() {
|
||||
let params = NetworkParams {
|
||||
session_id: "aa11".into(),
|
||||
tab_id: None,
|
||||
since: None,
|
||||
limit: None,
|
||||
max_text_chars: None,
|
||||
};
|
||||
let value = serde_json::to_value(¶ms).unwrap();
|
||||
assert_eq!(value["session_id"], "aa11");
|
||||
assert!(value.get("tab_id").is_none());
|
||||
assert!(value.get("since").is_none());
|
||||
assert!(value.get("limit").is_none());
|
||||
assert!(value.get("max_text_chars").is_none());
|
||||
let round: NetworkParams = serde_json::from_value(value).unwrap();
|
||||
assert_eq!(round, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_result_round_trips_response_and_failure() {
|
||||
let result = NetworkResult {
|
||||
tab_id: 7,
|
||||
entries: vec![
|
||||
NetworkEntry {
|
||||
sequence: 3,
|
||||
kind: NetworkEntryKind::Response,
|
||||
method: Some("GET".into()),
|
||||
url: "https://example.test/api".into(),
|
||||
status: Some(404),
|
||||
status_text: Some("Not Found".into()),
|
||||
mime_type: Some("application/json".into()),
|
||||
resource_type: Some("Fetch".into()),
|
||||
error_text: None,
|
||||
timestamp: Some(1234.5),
|
||||
truncated: false,
|
||||
},
|
||||
NetworkEntry {
|
||||
sequence: 4,
|
||||
kind: NetworkEntryKind::Failure,
|
||||
method: Some("GET".into()),
|
||||
url: "https://example.test/blocked".into(),
|
||||
status: None,
|
||||
status_text: None,
|
||||
mime_type: None,
|
||||
resource_type: Some("Script".into()),
|
||||
error_text: Some("net::ERR_BLOCKED_BY_CLIENT".into()),
|
||||
timestamp: Some(1240.0),
|
||||
truncated: false,
|
||||
},
|
||||
],
|
||||
next_since: 4,
|
||||
truncated: false,
|
||||
};
|
||||
let value = serde_json::to_value(&result).unwrap();
|
||||
assert_eq!(value["entries"][0]["kind"], json!("response"));
|
||||
assert_eq!(value["entries"][0]["status"], json!(404));
|
||||
assert_eq!(value["entries"][1]["kind"], json!("failure"));
|
||||
assert_eq!(
|
||||
value["entries"][1]["error_text"],
|
||||
json!("net::ERR_BLOCKED_BY_CLIENT")
|
||||
);
|
||||
let round: NetworkResult = serde_json::from_value(value).unwrap();
|
||||
assert_eq!(round, result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user