mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(request-help): add BSK_REQUEST_HELP=off to disable blocking help requests (#53)
* feat(request-help): add BSK_REQUEST_HELP=off to disable blocking help requests * style(extension): format HelpOutcome union per biome * docs(skill): condense BSK_REQUEST_HELP section
This commit is contained in:
@@ -518,7 +518,13 @@ export interface HelpTarget {
|
|||||||
selector?: string;
|
selector?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HelpOutcome = "continued" | "cancelled" | "timed_out" | "completed" | "navigated";
|
export type HelpOutcome =
|
||||||
|
| "continued"
|
||||||
|
| "cancelled"
|
||||||
|
| "timed_out"
|
||||||
|
| "completed"
|
||||||
|
| "navigated"
|
||||||
|
| "disabled";
|
||||||
|
|
||||||
export interface HelpCompletionCondition {
|
export interface HelpCompletionCondition {
|
||||||
url_contains?: string;
|
url_contains?: string;
|
||||||
|
|||||||
@@ -219,6 +219,11 @@ which refs/selectors matched a live element.
|
|||||||
control. After a `continued` or `completed` result, issue a separate
|
control. After a `continued` or `completed` result, issue a separate
|
||||||
observation tool call (usually `bsk snapshot --session <id>`) before using
|
observation tool call (usually `bsk snapshot --session <id>`) before using
|
||||||
new refs or reasoning about the post-help page state.
|
new refs or reasoning about the post-help page state.
|
||||||
|
#### Disabling request-help (unattended mode)
|
||||||
|
Set `BSK_REQUEST_HELP=off` on unattended servers: `bsk request-help` then
|
||||||
|
returns immediately with `outcome="disabled"` (no overlay, no waiting,
|
||||||
|
exit 0). Any other value keeps it enabled. If you get `disabled`, do not
|
||||||
|
retry — complete the task autonomously or stop gracefully.
|
||||||
|
|
||||||
### Recording — `bsk record`
|
### Recording — `bsk record`
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::time::Duration;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use bsk_protocol::Method;
|
use bsk_protocol::Method;
|
||||||
use bsk_protocol::tools::{
|
use bsk_protocol::tools::{
|
||||||
HelpCompletionCriteria, HelpTarget, RequestHelpParams, RequestHelpResult,
|
HelpCompletionCriteria, HelpOutcome, HelpTarget, RequestHelpParams, RequestHelpResult,
|
||||||
};
|
};
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
@@ -16,6 +16,31 @@ use crate::cli::ensure_daemon::ensure_daemon;
|
|||||||
use crate::cli::error::{CliError, Format};
|
use crate::cli::error::{CliError, Format};
|
||||||
use crate::cli::navigate::parse_timeout_ms;
|
use crate::cli::navigate::parse_timeout_ms;
|
||||||
|
|
||||||
|
/// Environment variable that disables the blocking `request-help`
|
||||||
|
/// human-in-loop tool, for unattended / background-server deployments
|
||||||
|
/// where a blocking help overlay must never appear.
|
||||||
|
pub(crate) const REQUEST_HELP_ENV: &str = "BSK_REQUEST_HELP";
|
||||||
|
|
||||||
|
/// Note attached to the synthetic result returned when request-help is
|
||||||
|
/// disabled, so the agent knows why no overlay was shown.
|
||||||
|
pub(crate) const REQUEST_HELP_DISABLED_NOTE: &str =
|
||||||
|
"request-help disabled by BSK_REQUEST_HELP=off (unattended mode)";
|
||||||
|
|
||||||
|
/// Whether `request-help` is disabled via [`REQUEST_HELP_ENV`]. Only the
|
||||||
|
/// value `off` (case-insensitive, surrounding whitespace ignored)
|
||||||
|
/// disables it; unset or any other value keeps the default enabled
|
||||||
|
/// behaviour.
|
||||||
|
pub(crate) fn request_help_disabled() -> bool {
|
||||||
|
is_off_value(std::env::var(REQUEST_HELP_ENV).ok().as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure value check behind [`request_help_disabled`], kept separate so it
|
||||||
|
/// can be unit-tested without touching process env (parallel tests race
|
||||||
|
/// on `std::env::set_var`, which is also `unsafe` in edition 2024).
|
||||||
|
fn is_off_value(value: Option<&str>) -> bool {
|
||||||
|
value.is_some_and(|v| v.trim().eq_ignore_ascii_case("off"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Args)]
|
#[derive(Debug, Clone, Args)]
|
||||||
pub struct RequestHelpArgs {
|
pub struct RequestHelpArgs {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -72,6 +97,18 @@ pub fn parse_target(raw: &str) -> HelpTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch(args: RequestHelpArgs, format: Format) -> Result<(), CliError> {
|
pub fn dispatch(args: RequestHelpArgs, format: Format) -> Result<(), CliError> {
|
||||||
|
// Disabled (`BSK_REQUEST_HELP=off`): return a synthetic `disabled`
|
||||||
|
// result immediately — no daemon startup, no overlay, no waiting.
|
||||||
|
if request_help_disabled() {
|
||||||
|
let result = RequestHelpResult {
|
||||||
|
outcome: HelpOutcome::Disabled,
|
||||||
|
completed_by: None,
|
||||||
|
note: Some(REQUEST_HELP_DISABLED_NOTE.into()),
|
||||||
|
tab_id: args.tab_id.unwrap_or(0),
|
||||||
|
resolved_targets: None,
|
||||||
|
};
|
||||||
|
return render(&result, format);
|
||||||
|
}
|
||||||
let info = ensure_daemon().context("ensure daemon is running")?;
|
let info = ensure_daemon().context("ensure daemon is running")?;
|
||||||
let targets: Vec<HelpTarget> = args.target.iter().map(|t| parse_target(t)).collect();
|
let targets: Vec<HelpTarget> = args.target.iter().map(|t| parse_target(t)).collect();
|
||||||
let completion_criteria = args
|
let completion_criteria = args
|
||||||
@@ -156,4 +193,19 @@ mod tests {
|
|||||||
assert_eq!(parse_target("email").selector.as_deref(), Some("email"));
|
assert_eq!(parse_target("email").selector.as_deref(), Some("email"));
|
||||||
assert!(parse_target("#login").ref_.is_none());
|
assert!(parse_target("#login").ref_.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn off_value_detection() {
|
||||||
|
assert!(is_off_value(Some("off")));
|
||||||
|
assert!(is_off_value(Some("OFF")));
|
||||||
|
assert!(is_off_value(Some("Off")));
|
||||||
|
assert!(is_off_value(Some(" off ")));
|
||||||
|
assert!(is_off_value(Some("\toff\n")));
|
||||||
|
assert!(!is_off_value(None));
|
||||||
|
assert!(!is_off_value(Some("")));
|
||||||
|
assert!(!is_off_value(Some("on")));
|
||||||
|
assert!(!is_off_value(Some("0")));
|
||||||
|
assert!(!is_off_value(Some("offx")));
|
||||||
|
assert!(!is_off_value(Some("of f")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ use bsk_protocol::system::{
|
|||||||
BrowserListParams, BrowserStatusEntry, SessionStatusEntry, StatusParams, StatusResult,
|
BrowserListParams, BrowserStatusEntry, SessionStatusEntry, StatusParams, StatusResult,
|
||||||
VersionSkewEntry,
|
VersionSkewEntry,
|
||||||
};
|
};
|
||||||
use bsk_protocol::tools::{ReturnFailure, WaitMsParams, WaitMsResult};
|
use bsk_protocol::tools::{
|
||||||
|
HelpOutcome, RequestHelpResult, ReturnFailure, WaitMsParams, WaitMsResult,
|
||||||
|
};
|
||||||
use bsk_protocol::{
|
use bsk_protocol::{
|
||||||
CancelParams, CancelResult, ErrorCode, Method, PingResult, ResponseBody, RpcError, RpcId,
|
CancelParams, CancelResult, ErrorCode, Method, PingResult, ResponseBody, RpcError, RpcId,
|
||||||
};
|
};
|
||||||
@@ -280,6 +282,19 @@ async fn handle_tool_dispatch(
|
|||||||
method: Method,
|
method: Method,
|
||||||
params: Value,
|
params: Value,
|
||||||
) -> ResponseBody {
|
) -> ResponseBody {
|
||||||
|
// `BSK_REQUEST_HELP=off` (unattended mode): never forward the
|
||||||
|
// blocking human-in-loop call to the extension; answer immediately
|
||||||
|
// with a synthetic `disabled` result.
|
||||||
|
if method == Method::ToolRequestHelp && crate::cli::human_loop::request_help_disabled() {
|
||||||
|
let result = RequestHelpResult {
|
||||||
|
outcome: HelpOutcome::Disabled,
|
||||||
|
completed_by: None,
|
||||||
|
note: Some(crate::cli::human_loop::REQUEST_HELP_DISABLED_NOTE.into()),
|
||||||
|
tab_id: params.get("tab_id").and_then(Value::as_i64).unwrap_or(0),
|
||||||
|
resolved_targets: None,
|
||||||
|
};
|
||||||
|
return ResponseBody::Ok(serde_json::to_value(result).unwrap_or(Value::Null));
|
||||||
|
}
|
||||||
let session_id = match params.get("session_id").and_then(|v| v.as_str()) {
|
let session_id = match params.get("session_id").and_then(|v| v.as_str()) {
|
||||||
Some(s) if !s.is_empty() => SessionId(s.to_string()),
|
Some(s) if !s.is_empty() => SessionId(s.to_string()),
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -83,6 +83,13 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"navigated"
|
"navigated"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "request-help is disabled by configuration (`BSK_REQUEST_HELP=off`): the call returns immediately without ever showing the overlay.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"disabled"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ pub enum HelpOutcome {
|
|||||||
/// The page navigated while waiting (full reload or SPA URL change).
|
/// The page navigated while waiting (full reload or SPA URL change).
|
||||||
/// Deprecated: navigation alone should not end a help request.
|
/// Deprecated: navigation alone should not end a help request.
|
||||||
Navigated,
|
Navigated,
|
||||||
|
/// request-help is disabled by configuration (`BSK_REQUEST_HELP=off`):
|
||||||
|
/// the call returns immediately without ever showing the overlay.
|
||||||
|
Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HelpOutcome {
|
impl HelpOutcome {
|
||||||
@@ -105,6 +108,7 @@ impl HelpOutcome {
|
|||||||
Self::TimedOut => "timed_out",
|
Self::TimedOut => "timed_out",
|
||||||
Self::Completed => "completed",
|
Self::Completed => "completed",
|
||||||
Self::Navigated => "navigated",
|
Self::Navigated => "navigated",
|
||||||
|
Self::Disabled => "disabled",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,6 +206,10 @@ mod tests {
|
|||||||
serde_json::to_value(HelpOutcome::Navigated).unwrap(),
|
serde_json::to_value(HelpOutcome::Navigated).unwrap(),
|
||||||
json!("navigated")
|
json!("navigated")
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(HelpOutcome::Disabled).unwrap(),
|
||||||
|
json!("disabled")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -219,6 +219,11 @@ which refs/selectors matched a live element.
|
|||||||
control. After a `continued` or `completed` result, issue a separate
|
control. After a `continued` or `completed` result, issue a separate
|
||||||
observation tool call (usually `bsk snapshot --session <id>`) before using
|
observation tool call (usually `bsk snapshot --session <id>`) before using
|
||||||
new refs or reasoning about the post-help page state.
|
new refs or reasoning about the post-help page state.
|
||||||
|
#### Disabling request-help (unattended mode)
|
||||||
|
Set `BSK_REQUEST_HELP=off` on unattended servers: `bsk request-help` then
|
||||||
|
returns immediately with `outcome="disabled"` (no overlay, no waiting,
|
||||||
|
exit 0). Any other value keeps it enabled. If you get `disabled`, do not
|
||||||
|
retry — complete the task autonomously or stop gracefully.
|
||||||
|
|
||||||
### Recording — `bsk record`
|
### Recording — `bsk record`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user