refactor(service): unify submit bootstrap target resolution
Use page context, deterministic plans, and direct-skill metadata as the service-owned bootstrap target precedence so callback-host startup no longer relies on line-loss text matching or the old request-url helper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
66
Cargo.lock
generated
66
Cargo.lock
generated
@@ -2339,6 +2339,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -2377,6 +2386,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml 0.8.23",
|
||||||
"tungstenite 0.29.0",
|
"tungstenite 0.29.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zeroclawlabs",
|
"zeroclawlabs",
|
||||||
@@ -2740,6 +2750,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -2748,13 +2770,22 @@ checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned 1.1.1",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -2764,6 +2795,20 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_write",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -2773,6 +2818,12 @@ dependencies = [
|
|||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -3444,6 +3495,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -3637,7 +3697,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-tungstenite 0.29.0",
|
"tokio-tungstenite 0.29.0",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ serde_json = "1"
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||||
|
toml = "0.8"
|
||||||
tungstenite = "0.29"
|
tungstenite = "0.29"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
|
zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::browser::ws_backend::WsBrowserBackend;
|
use crate::browser::ws_backend::WsBrowserBackend;
|
||||||
use crate::browser::{BrowserBackend, PipeBrowserBackend};
|
use crate::browser::{BrowserBackend, PipeBrowserBackend};
|
||||||
|
use crate::config::SgClawSettings;
|
||||||
use crate::pipe::{BrowserMessage, BrowserPipeTool, PipeError, Transport};
|
use crate::pipe::{BrowserMessage, BrowserPipeTool, PipeError, Transport};
|
||||||
|
|
||||||
pub use task_runner::{
|
pub use task_runner::{
|
||||||
@@ -22,13 +23,27 @@ fn browser_backend_for_submit<T: Transport + 'static>(
|
|||||||
request: &SubmitTaskRequest,
|
request: &SubmitTaskRequest,
|
||||||
) -> Result<Arc<dyn BrowserBackend>, PipeError> {
|
) -> Result<Arc<dyn BrowserBackend>, PipeError> {
|
||||||
if let Some(browser_ws_url) = configured_browser_ws_url(context) {
|
if let Some(browser_ws_url) = configured_browser_ws_url(context) {
|
||||||
|
let settings = context.load_sgclaw_settings()?.unwrap_or(
|
||||||
|
SgClawSettings::from_legacy_deepseek_fields(
|
||||||
|
"test-key".to_string(),
|
||||||
|
"https://example.invalid".to_string(),
|
||||||
|
"test-model".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.map_err(|err| PipeError::Protocol(err.to_string()))?,
|
||||||
|
);
|
||||||
|
let bootstrap_target = crate::service::server::resolve_submit_bootstrap_target(
|
||||||
|
request,
|
||||||
|
context.workspace_root(),
|
||||||
|
&settings,
|
||||||
|
);
|
||||||
return Ok(Arc::new(
|
return Ok(Arc::new(
|
||||||
WsBrowserBackend::new(
|
WsBrowserBackend::new(
|
||||||
Arc::new(crate::service::browser_ws_client::ServiceWsClient::connect(
|
Arc::new(crate::service::browser_ws_client::ServiceWsClient::connect(
|
||||||
&browser_ws_url,
|
&browser_ws_url,
|
||||||
)?),
|
)?),
|
||||||
browser_tool.mac_policy().clone(),
|
browser_tool.mac_policy().clone(),
|
||||||
crate::service::browser_ws_client::initial_request_url_for_submit_task(request),
|
bootstrap_target.request_url,
|
||||||
)
|
)
|
||||||
.with_response_timeout(browser_tool.response_timeout()),
|
.with_response_timeout(browser_tool.response_timeout()),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ impl AgentRuntimeContext {
|
|||||||
self.config_path.as_deref()
|
self.config_path.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn workspace_root(&self) -> &Path {
|
||||||
|
&self.workspace_root
|
||||||
|
}
|
||||||
|
|
||||||
fn settings_source_label(&self) -> String {
|
fn settings_source_label(&self) -> String {
|
||||||
match &self.config_path {
|
match &self.config_path {
|
||||||
Some(path) if path.exists() => path.display().to_string(),
|
Some(path) if path.exists() => path.display().to_string(),
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use zeroclaw::skills::{load_skills_from_directory, SkillTool};
|
use zeroclaw::skills::{load_skills_from_directory, SkillTool};
|
||||||
|
|
||||||
@@ -12,6 +14,12 @@ use crate::compat::runtime::CompatTaskContext;
|
|||||||
use crate::config::SgClawSettings;
|
use crate::config::SgClawSettings;
|
||||||
use crate::pipe::{BrowserPipeTool, PipeError, Transport};
|
use crate::pipe::{BrowserPipeTool, PipeError, Transport};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct DirectSubmitBootstrapMetadata {
|
||||||
|
pub bootstrap_url: String,
|
||||||
|
pub expected_domain: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct DirectSubmitOutcome {
|
pub struct DirectSubmitOutcome {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
@@ -111,6 +119,32 @@ pub fn execute_browser_script_skill_raw_output_with_browser_backend(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_direct_submit_bootstrap_metadata(
|
||||||
|
configured_tool: &str,
|
||||||
|
workspace_root: &Path,
|
||||||
|
settings: &SgClawSettings,
|
||||||
|
) -> Result<Option<DirectSubmitBootstrapMetadata>, PipeError> {
|
||||||
|
let (tool, skill_root) = resolve_browser_script_skill(configured_tool, workspace_root, settings)?;
|
||||||
|
let manifest_path = skill_root.join("SKILL.toml");
|
||||||
|
let Ok(manifest) = fs::read_to_string(&manifest_path) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Ok(parsed) = toml::from_str::<DirectSubmitSkillManifest>(&manifest) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata = parsed
|
||||||
|
.tools
|
||||||
|
.into_iter()
|
||||||
|
.find(|candidate| candidate.name == tool.name)
|
||||||
|
.and_then(|candidate| candidate.metadata)
|
||||||
|
.and_then(|metadata| {
|
||||||
|
normalize_bootstrap_metadata(metadata.bootstrap_url, metadata.expected_domain)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_browser_script_skill(
|
fn resolve_browser_script_skill(
|
||||||
configured_tool: &str,
|
configured_tool: &str,
|
||||||
workspace_root: &Path,
|
workspace_root: &Path,
|
||||||
@@ -306,6 +340,50 @@ fn count_summary_rows(counts: Option<&Value>, sections: Option<&Value>) -> usize
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DirectSubmitSkillManifest {
|
||||||
|
#[serde(default)]
|
||||||
|
tools: Vec<DirectSubmitSkillManifestTool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DirectSubmitSkillManifestTool {
|
||||||
|
name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
metadata: Option<DirectSubmitToolMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DirectSubmitToolMetadata {
|
||||||
|
#[serde(default)]
|
||||||
|
bootstrap_url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
expected_domain: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_bootstrap_metadata(
|
||||||
|
bootstrap_url: Option<String>,
|
||||||
|
expected_domain: Option<String>,
|
||||||
|
) -> Option<DirectSubmitBootstrapMetadata> {
|
||||||
|
let bootstrap_url = bootstrap_url
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())?;
|
||||||
|
let parsed = Url::parse(bootstrap_url).ok()?;
|
||||||
|
if parsed.scheme().is_empty() || parsed.host_str().is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(DirectSubmitBootstrapMetadata {
|
||||||
|
bootstrap_url: parsed.to_string(),
|
||||||
|
expected_domain: expected_domain
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToString::to_string),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_configured_tool_name(configured_tool: &str) -> Result<(&str, &str), PipeError> {
|
fn parse_configured_tool_name(configured_tool: &str) -> Result<(&str, &str), PipeError> {
|
||||||
let (skill_name, tool_name) = configured_tool.split_once('.').ok_or_else(|| {
|
let (skill_name, tool_name) = configured_tool.split_once('.').ok_or_else(|| {
|
||||||
PipeError::Protocol(format!(
|
PipeError::Protocol(format!(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub use protocol::{ClientMessage, ConfigUpdatePayload, ServiceMessage};
|
|||||||
pub use server::{ServiceEventSink, ServiceSession};
|
pub use server::{ServiceEventSink, ServiceSession};
|
||||||
|
|
||||||
pub(crate) mod browser_ws_client {
|
pub(crate) mod browser_ws_client {
|
||||||
pub(crate) use super::server::{initial_request_url_for_submit_task, ServiceWsClient};
|
pub(crate) use super::server::ServiceWsClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::path::PathBuf;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::Url;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -28,6 +29,7 @@ use crate::browser::bridge_transport::BridgeActionTransport;
|
|||||||
use crate::browser::{BrowserBackend, BrowserCallbackBackend};
|
use crate::browser::{BrowserBackend, BrowserCallbackBackend};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::browser::BridgeBrowserBackend;
|
use crate::browser::BridgeBrowserBackend;
|
||||||
|
use crate::config::SgClawSettings;
|
||||||
use crate::pipe::{AgentMessage, BrowserMessage, PipeError, Transport};
|
use crate::pipe::{AgentMessage, BrowserMessage, PipeError, Transport};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::pipe::Timing;
|
use crate::pipe::Timing;
|
||||||
@@ -329,7 +331,21 @@ pub(crate) fn serve_client(
|
|||||||
// Lazily create and cache the browser callback host. On first
|
// Lazily create and cache the browser callback host. On first
|
||||||
// task it opens the helper page; subsequent tasks reuse it.
|
// task it opens the helper page; subsequent tasks reuse it.
|
||||||
if cached_host.is_none() {
|
if cached_host.is_none() {
|
||||||
let bootstrap_url = initial_request_url_for_submit_task(&request);
|
let settings = context.load_sgclaw_settings()?.unwrap_or(
|
||||||
|
SgClawSettings::from_legacy_deepseek_fields(
|
||||||
|
"test-key".to_string(),
|
||||||
|
"https://example.invalid".to_string(),
|
||||||
|
"test-model".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.map_err(|err| PipeError::Protocol(err.to_string()))?,
|
||||||
|
);
|
||||||
|
let bootstrap_target = resolve_submit_bootstrap_target(
|
||||||
|
&request,
|
||||||
|
context.workspace_root(),
|
||||||
|
&settings,
|
||||||
|
);
|
||||||
|
let bootstrap_url = bootstrap_target.request_url;
|
||||||
match LiveBrowserCallbackHost::start_with_browser_ws_url(
|
match LiveBrowserCallbackHost::start_with_browser_ws_url(
|
||||||
browser_ws_url,
|
browser_ws_url,
|
||||||
&bootstrap_url,
|
&bootstrap_url,
|
||||||
@@ -419,15 +435,82 @@ pub(crate) fn serve_client(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn initial_request_url_for_submit_task(request: &crate::agent::SubmitTaskRequest) -> String {
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
request
|
pub(crate) struct SubmitBootstrapTarget {
|
||||||
|
pub request_url: String,
|
||||||
|
pub expected_domain: Option<String>,
|
||||||
|
pub source: BootstrapTargetSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum BootstrapTargetSource {
|
||||||
|
PageContext,
|
||||||
|
DeterministicPlan,
|
||||||
|
SkillConfig,
|
||||||
|
Fallback,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_submit_bootstrap_target(
|
||||||
|
request: &crate::agent::SubmitTaskRequest,
|
||||||
|
workspace_root: &Path,
|
||||||
|
settings: &SgClawSettings,
|
||||||
|
) -> SubmitBootstrapTarget {
|
||||||
|
if let Some(page_url) = request
|
||||||
.page_url
|
.page_url
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.map(ToString::to_string)
|
{
|
||||||
.or_else(|| derive_request_url_from_instruction(&request.instruction))
|
return SubmitBootstrapTarget {
|
||||||
.unwrap_or_else(|| "about:blank".to_string())
|
request_url: page_url.to_string(),
|
||||||
|
expected_domain: Url::parse(page_url)
|
||||||
|
.ok()
|
||||||
|
.and_then(|url| url.domain().map(ToString::to_string)),
|
||||||
|
source: BootstrapTargetSource::PageContext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let crate::compat::deterministic_submit::DeterministicSubmitDecision::Execute(plan) =
|
||||||
|
crate::compat::deterministic_submit::decide_deterministic_submit(
|
||||||
|
&request.instruction,
|
||||||
|
request.page_url.as_deref(),
|
||||||
|
request.page_title.as_deref(),
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return SubmitBootstrapTarget {
|
||||||
|
request_url: plan.target_url.clone(),
|
||||||
|
expected_domain: Some(plan.expected_domain.clone()),
|
||||||
|
source: BootstrapTargetSource::DeterministicPlan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(configured_tool) = settings
|
||||||
|
.direct_submit_skill
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
if let Ok(Some(metadata)) =
|
||||||
|
crate::compat::direct_skill_runtime::resolve_direct_submit_bootstrap_metadata(
|
||||||
|
configured_tool,
|
||||||
|
workspace_root,
|
||||||
|
settings,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return SubmitBootstrapTarget {
|
||||||
|
request_url: metadata.bootstrap_url,
|
||||||
|
expected_domain: metadata.expected_domain,
|
||||||
|
source: BootstrapTargetSource::SkillConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SubmitBootstrapTarget {
|
||||||
|
request_url: derive_request_url_from_instruction(&request.instruction)
|
||||||
|
.unwrap_or_else(|| "about:blank".to_string()),
|
||||||
|
expected_domain: None,
|
||||||
|
source: BootstrapTargetSource::Fallback,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_request_url_from_instruction(instruction: &str) -> Option<String> {
|
fn derive_request_url_from_instruction(instruction: &str) -> Option<String> {
|
||||||
@@ -457,12 +540,6 @@ fn derive_request_url_from_instruction(instruction: &str) -> Option<String> {
|
|||||||
return Some("https://zhuanlan.zhihu.com".to_string());
|
return Some("https://zhuanlan.zhihu.com".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 台区线损相关
|
|
||||||
// TODO: 临时方案,后续应从 skill 配置或 deterministic_submit 解析结果中获取
|
|
||||||
if instruction.contains("线损") || instruction.contains("lineloss") {
|
|
||||||
return Some("http://20.76.57.61:18080".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,16 +911,77 @@ fn write_http_json_response(stream: &mut impl std::io::Write, status: &str, body
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::sync::{mpsc, Arc};
|
use std::sync::{mpsc, Arc};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::agent::SubmitTaskRequest;
|
use crate::agent::SubmitTaskRequest;
|
||||||
use crate::browser::BrowserBackend;
|
use crate::browser::BrowserBackend;
|
||||||
use crate::pipe::Action;
|
use crate::pipe::Action;
|
||||||
|
|
||||||
|
fn service_test_settings(
|
||||||
|
skills_dir: Option<PathBuf>,
|
||||||
|
direct_submit_skill: Option<&str>,
|
||||||
|
) -> SgClawSettings {
|
||||||
|
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||||||
|
"test-key".to_string(),
|
||||||
|
"https://example.invalid".to_string(),
|
||||||
|
"test-model".to_string(),
|
||||||
|
skills_dir,
|
||||||
|
)
|
||||||
|
.expect("settings");
|
||||||
|
settings.direct_submit_skill = direct_submit_skill.map(ToString::to_string);
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
|
||||||
|
fn staged_skill_staging_dir() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../claw/claw/skills/skill_staging/skills")
|
||||||
|
.canonicalize()
|
||||||
|
.expect("staged skills dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temp_direct_submit_skill_root(bootstrap_url: &str) -> PathBuf {
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"sgclaw-bootstrap-target-skill-root-{}",
|
||||||
|
Uuid::new_v4()
|
||||||
|
));
|
||||||
|
let skill_dir = root.join("fault-details-report");
|
||||||
|
let script_dir = skill_dir.join("scripts");
|
||||||
|
fs::create_dir_all(&script_dir).expect("create script dir");
|
||||||
|
fs::write(
|
||||||
|
skill_dir.join("SKILL.toml"),
|
||||||
|
format!(
|
||||||
|
r#"[skill]
|
||||||
|
name = "fault-details-report"
|
||||||
|
description = "Collect 95598 fault detail data via browser eval."
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[tools]]
|
||||||
|
name = "collect_fault_details"
|
||||||
|
description = "Collect structured fault detail rows for a specific period."
|
||||||
|
kind = "browser_script"
|
||||||
|
command = "scripts/collect_fault_details.js"
|
||||||
|
|
||||||
|
[tools.metadata]
|
||||||
|
bootstrap_url = "{bootstrap_url}"
|
||||||
|
expected_domain = "95598.sgcc.com.cn"
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("write skill manifest");
|
||||||
|
fs::write(
|
||||||
|
script_dir.join("collect_fault_details.js"),
|
||||||
|
"return { ok: true };\n",
|
||||||
|
)
|
||||||
|
.expect("write skill script");
|
||||||
|
root
|
||||||
|
}
|
||||||
|
|
||||||
fn service_test_policy() -> MacPolicy {
|
fn service_test_policy() -> MacPolicy {
|
||||||
MacPolicy::from_json_str(
|
MacPolicy::from_json_str(
|
||||||
r#"{
|
r#"{
|
||||||
@@ -859,56 +997,165 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initial_request_url_prefers_submit_task_page_url() {
|
fn page_context_bootstrap_target_wins_over_deterministic_and_skill_fallback() {
|
||||||
let request = SubmitTaskRequest {
|
let request = SubmitTaskRequest {
|
||||||
instruction: "打开知乎热榜".to_string(),
|
instruction: "兰州公司 台区线损大数据 月累计线损率统计分析。。。".to_string(),
|
||||||
page_url: Some(" https://www.zhihu.com/ ".to_string()),
|
page_url: Some(" https://already-open.example.com/page ".to_string()),
|
||||||
..SubmitTaskRequest::default()
|
..SubmitTaskRequest::default()
|
||||||
};
|
};
|
||||||
|
let settings = SgClawSettings::from_legacy_deepseek_fields(
|
||||||
|
"test-key".to_string(),
|
||||||
|
"https://example.invalid".to_string(),
|
||||||
|
"test-model".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("settings");
|
||||||
|
|
||||||
assert_eq!(
|
let target = resolve_submit_bootstrap_target(&request, Path::new("."), &settings);
|
||||||
initial_request_url_for_submit_task(&request),
|
|
||||||
"https://www.zhihu.com/"
|
assert_eq!(target.request_url, "https://already-open.example.com/page");
|
||||||
);
|
assert_eq!(target.expected_domain.as_deref(), Some("already-open.example.com"));
|
||||||
|
assert_eq!(target.source, BootstrapTargetSource::PageContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initial_request_url_falls_back_to_zhihu_origin_for_hotlist_routes() {
|
fn whitespace_page_url_does_not_short_circuit_bootstrap_fallback() {
|
||||||
let request = SubmitTaskRequest {
|
let request = SubmitTaskRequest {
|
||||||
instruction: "打开知乎热榜,获取前10条数据,并导出 Excel".to_string(),
|
instruction: "打开知乎热榜,获取前10条数据,并导出 Excel".to_string(),
|
||||||
|
page_url: Some(" ".to_string()),
|
||||||
..SubmitTaskRequest::default()
|
..SubmitTaskRequest::default()
|
||||||
};
|
};
|
||||||
|
let settings = SgClawSettings::from_legacy_deepseek_fields(
|
||||||
|
"test-key".to_string(),
|
||||||
|
"https://example.invalid".to_string(),
|
||||||
|
"test-model".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("settings");
|
||||||
|
|
||||||
assert_eq!(
|
let target = resolve_submit_bootstrap_target(&request, Path::new("."), &settings);
|
||||||
initial_request_url_for_submit_task(&request),
|
|
||||||
"https://www.zhihu.com"
|
assert_eq!(target.request_url, "https://www.zhihu.com");
|
||||||
);
|
assert_eq!(target.source, BootstrapTargetSource::Fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initial_request_url_falls_back_to_zhihu_origin_for_generated_article_publish_routes() {
|
fn deterministic_bootstrap_target_uses_plan_target_url() {
|
||||||
let request = SubmitTaskRequest {
|
|
||||||
instruction: "在知乎自动发表一篇名称为人工智能技能大全".to_string(),
|
|
||||||
..SubmitTaskRequest::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
initial_request_url_for_submit_task(&request),
|
|
||||||
"https://www.zhihu.com"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn initial_request_url_falls_back_to_lineloss_origin_for_lineloss_instructions() {
|
|
||||||
let request = SubmitTaskRequest {
|
let request = SubmitTaskRequest {
|
||||||
instruction: "兰州公司 台区线损大数据 月累计线损率统计分析。。。".to_string(),
|
instruction: "兰州公司 台区线损大数据 月累计线损率统计分析。。。".to_string(),
|
||||||
..SubmitTaskRequest::default()
|
..SubmitTaskRequest::default()
|
||||||
};
|
};
|
||||||
|
let settings = service_test_settings(None, None);
|
||||||
|
|
||||||
|
let target = resolve_submit_bootstrap_target(&request, Path::new("."), &settings);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
initial_request_url_for_submit_task(&request),
|
target.request_url,
|
||||||
"http://20.76.57.61:18080"
|
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor"
|
||||||
);
|
);
|
||||||
|
assert_eq!(target.expected_domain.as_deref(), Some("20.76.57.61"));
|
||||||
|
assert_eq!(target.source, BootstrapTargetSource::DeterministicPlan);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_metadata_bootstrap_url_is_used_when_no_page_context_or_plan_exists() {
|
||||||
|
let request = SubmitTaskRequest {
|
||||||
|
instruction: "请采集 2026-03 的故障明细并返回结果".to_string(),
|
||||||
|
..SubmitTaskRequest::default()
|
||||||
|
};
|
||||||
|
let settings = service_test_settings(
|
||||||
|
Some(staged_skill_staging_dir()),
|
||||||
|
Some("fault-details-report.collect_fault_details"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let target = resolve_submit_bootstrap_target(&request, Path::new("."), &settings);
|
||||||
|
|
||||||
|
assert_eq!(target.request_url, "https://95598.sgcc.com.cn/");
|
||||||
|
assert_eq!(target.expected_domain.as_deref(), Some("95598.sgcc.com.cn"));
|
||||||
|
assert_eq!(target.source, BootstrapTargetSource::SkillConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_skill_bootstrap_url_falls_back_to_about_blank() {
|
||||||
|
let request = SubmitTaskRequest {
|
||||||
|
instruction: "请采集 2026-03 的故障明细并返回结果".to_string(),
|
||||||
|
..SubmitTaskRequest::default()
|
||||||
|
};
|
||||||
|
let skills_dir = temp_direct_submit_skill_root("not-a-valid-absolute-url");
|
||||||
|
let settings = service_test_settings(
|
||||||
|
Some(skills_dir.clone()),
|
||||||
|
Some("fault-details-report.collect_fault_details"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let target = resolve_submit_bootstrap_target(&request, Path::new("."), &settings);
|
||||||
|
|
||||||
|
assert_eq!(target.request_url, "about:blank");
|
||||||
|
assert_eq!(target.expected_domain, None);
|
||||||
|
assert_eq!(target.source, BootstrapTargetSource::Fallback);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(skills_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bootstrap_target_precedence_matrix_covers_page_context_deterministic_skill_and_fallback() {
|
||||||
|
let page_request = SubmitTaskRequest {
|
||||||
|
instruction: "兰州公司 台区线损大数据 月累计线损率统计分析。。。".to_string(),
|
||||||
|
page_url: Some(" https://already-open.example.com/page ".to_string()),
|
||||||
|
..SubmitTaskRequest::default()
|
||||||
|
};
|
||||||
|
let page_settings = service_test_settings(
|
||||||
|
Some(staged_skill_staging_dir()),
|
||||||
|
Some("fault-details-report.collect_fault_details"),
|
||||||
|
);
|
||||||
|
let page_target =
|
||||||
|
resolve_submit_bootstrap_target(&page_request, Path::new("."), &page_settings);
|
||||||
|
assert_eq!(page_target.request_url, "https://already-open.example.com/page");
|
||||||
|
assert_eq!(page_target.source, BootstrapTargetSource::PageContext);
|
||||||
|
|
||||||
|
let deterministic_request = SubmitTaskRequest {
|
||||||
|
instruction: "兰州公司 台区线损大数据 月累计线损率统计分析。。。".to_string(),
|
||||||
|
..SubmitTaskRequest::default()
|
||||||
|
};
|
||||||
|
let deterministic_settings = service_test_settings(
|
||||||
|
Some(staged_skill_staging_dir()),
|
||||||
|
Some("fault-details-report.collect_fault_details"),
|
||||||
|
);
|
||||||
|
let deterministic_target = resolve_submit_bootstrap_target(
|
||||||
|
&deterministic_request,
|
||||||
|
Path::new("."),
|
||||||
|
&deterministic_settings,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deterministic_target.request_url,
|
||||||
|
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deterministic_target.source,
|
||||||
|
BootstrapTargetSource::DeterministicPlan
|
||||||
|
);
|
||||||
|
|
||||||
|
let skill_request = SubmitTaskRequest {
|
||||||
|
instruction: "请采集 2026-03 的故障明细并返回结果".to_string(),
|
||||||
|
..SubmitTaskRequest::default()
|
||||||
|
};
|
||||||
|
let skill_settings = service_test_settings(
|
||||||
|
Some(staged_skill_staging_dir()),
|
||||||
|
Some("fault-details-report.collect_fault_details"),
|
||||||
|
);
|
||||||
|
let skill_target =
|
||||||
|
resolve_submit_bootstrap_target(&skill_request, Path::new("."), &skill_settings);
|
||||||
|
assert_eq!(skill_target.request_url, "https://95598.sgcc.com.cn/");
|
||||||
|
assert_eq!(skill_target.source, BootstrapTargetSource::SkillConfig);
|
||||||
|
|
||||||
|
let fallback_request = SubmitTaskRequest {
|
||||||
|
instruction: "完全不相关的普通问题".to_string(),
|
||||||
|
..SubmitTaskRequest::default()
|
||||||
|
};
|
||||||
|
let fallback_settings = service_test_settings(None, None);
|
||||||
|
let fallback_target =
|
||||||
|
resolve_submit_bootstrap_target(&fallback_request, Path::new("."), &fallback_settings);
|
||||||
|
assert_eq!(fallback_target.request_url, "about:blank");
|
||||||
|
assert_eq!(fallback_target.source, BootstrapTargetSource::Fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -194,12 +194,34 @@ fn start_callback_host_hotlist_browser_server(
|
|||||||
.send(CallbackHostBrowserEvent::BrowserFrame(first_action.clone()))
|
.send(CallbackHostBrowserEvent::BrowserFrame(first_action.clone()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let Some(values) = first_action.as_array() else {
|
let second_action = match websocket.read().unwrap() {
|
||||||
|
Message::Text(text) => serde_json::from_str::<Value>(&text).unwrap(),
|
||||||
|
other => panic!("expected second browser action frame, got {other:?}"),
|
||||||
|
};
|
||||||
|
event_tx
|
||||||
|
.send(CallbackHostBrowserEvent::BrowserFrame(second_action.clone()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let Some(close_values) = first_action.as_array() else {
|
||||||
|
websocket.close(None).ok();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let is_helper_close = close_values.len() >= 3
|
||||||
|
&& close_values[1] == json!("sgHideBrowerserClosePage")
|
||||||
|
&& close_values[2]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html"));
|
||||||
|
if !is_helper_close {
|
||||||
|
websocket.close(None).ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(values) = second_action.as_array() else {
|
||||||
websocket.close(None).ok();
|
websocket.close(None).ok();
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let is_helper_open = values.len() >= 3
|
let is_helper_open = values.len() >= 3
|
||||||
&& values[1] == json!("sgBrowerserOpenPage")
|
&& values[1] == json!("sgHideBrowerserOpenPage")
|
||||||
&& values[2]
|
&& values[2]
|
||||||
.as_str()
|
.as_str()
|
||||||
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html"));
|
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html"));
|
||||||
@@ -873,6 +895,7 @@ fn service_binary_submit_flow_routes_zhihu_through_callback_host() {
|
|||||||
browser_server.join().unwrap();
|
browser_server.join().unwrap();
|
||||||
|
|
||||||
let register = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
let register = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
|
let bootstrap_close = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
let bootstrap = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
let bootstrap = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
let pre_ready = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
let pre_ready = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
let open_page = event_rx.recv_timeout(Duration::from_secs(4)).unwrap();
|
let open_page = event_rx.recv_timeout(Duration::from_secs(4)).unwrap();
|
||||||
@@ -902,12 +925,22 @@ fn service_binary_submit_flow_routes_zhihu_through_callback_host() {
|
|||||||
};
|
};
|
||||||
assert_eq!(register, json!({ "type": "register", "role": "web" }));
|
assert_eq!(register, json!({ "type": "register", "role": "web" }));
|
||||||
|
|
||||||
|
let bootstrap_close = match bootstrap_close {
|
||||||
|
CallbackHostBrowserEvent::BrowserFrame(value) => value,
|
||||||
|
other => panic!("expected helper close frame, got {other:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(bootstrap_close[0], json!("https://www.zhihu.com"));
|
||||||
|
assert_eq!(bootstrap_close[1], json!("sgHideBrowerserClosePage"));
|
||||||
|
assert!(bootstrap_close[2]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html")));
|
||||||
|
|
||||||
let bootstrap = match bootstrap {
|
let bootstrap = match bootstrap {
|
||||||
CallbackHostBrowserEvent::BrowserFrame(value) => value,
|
CallbackHostBrowserEvent::BrowserFrame(value) => value,
|
||||||
other => panic!("expected helper bootstrap frame, got {other:?}"),
|
other => panic!("expected helper bootstrap frame, got {other:?}"),
|
||||||
};
|
};
|
||||||
assert_eq!(bootstrap[0], json!("https://www.zhihu.com"));
|
assert_eq!(bootstrap[0], json!("https://www.zhihu.com"));
|
||||||
assert_eq!(bootstrap[1], json!("sgBrowerserOpenPage"));
|
assert_eq!(bootstrap[1], json!("sgHideBrowerserOpenPage"));
|
||||||
assert!(bootstrap[2]
|
assert!(bootstrap[2]
|
||||||
.as_str()
|
.as_str()
|
||||||
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html")));
|
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html")));
|
||||||
@@ -1046,6 +1079,7 @@ fn service_binary_submit_flow_uses_callback_host_command_semantics_for_zhihu() {
|
|||||||
browser_server.join().unwrap();
|
browser_server.join().unwrap();
|
||||||
|
|
||||||
let register = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
let register = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
|
let bootstrap_close = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
let bootstrap = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
let bootstrap = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
let pre_ready = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
let pre_ready = event_rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||||
let open_page = event_rx.recv_timeout(Duration::from_secs(4)).unwrap();
|
let open_page = event_rx.recv_timeout(Duration::from_secs(4)).unwrap();
|
||||||
@@ -1075,12 +1109,22 @@ fn service_binary_submit_flow_uses_callback_host_command_semantics_for_zhihu() {
|
|||||||
};
|
};
|
||||||
assert_eq!(register, json!({ "type": "register", "role": "web" }));
|
assert_eq!(register, json!({ "type": "register", "role": "web" }));
|
||||||
|
|
||||||
|
let bootstrap_close = match bootstrap_close {
|
||||||
|
CallbackHostBrowserEvent::BrowserFrame(value) => value,
|
||||||
|
other => panic!("expected helper close frame, got {other:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(bootstrap_close[0], json!("https://www.zhihu.com"));
|
||||||
|
assert_eq!(bootstrap_close[1], json!("sgHideBrowerserClosePage"));
|
||||||
|
assert!(bootstrap_close[2]
|
||||||
|
.as_str()
|
||||||
|
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html")));
|
||||||
|
|
||||||
let bootstrap = match bootstrap {
|
let bootstrap = match bootstrap {
|
||||||
CallbackHostBrowserEvent::BrowserFrame(value) => value,
|
CallbackHostBrowserEvent::BrowserFrame(value) => value,
|
||||||
other => panic!("expected helper bootstrap frame, got {other:?}"),
|
other => panic!("expected helper bootstrap frame, got {other:?}"),
|
||||||
};
|
};
|
||||||
assert_eq!(bootstrap[0], json!("https://www.zhihu.com"));
|
assert_eq!(bootstrap[0], json!("https://www.zhihu.com"));
|
||||||
assert_eq!(bootstrap[1], json!("sgBrowerserOpenPage"));
|
assert_eq!(bootstrap[1], json!("sgHideBrowerserOpenPage"));
|
||||||
assert!(bootstrap[2]
|
assert!(bootstrap[2]
|
||||||
.as_str()
|
.as_str()
|
||||||
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html")));
|
.is_some_and(|url| url.ends_with("/sgclaw/browser-helper.html")));
|
||||||
|
|||||||
Reference in New Issue
Block a user