feat: align browser callback runtime and export flows

Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-06 21:44:53 +08:00
parent 0dd655712c
commit bdf8e12246
55 changed files with 14440 additions and 1053 deletions

View File

@@ -0,0 +1,145 @@
mod common;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use common::MockTransport;
use sgclaw::browser::{BrowserBackend, PipeBrowserBackend};
use sgclaw::compat::browser_script_skill_tool::build_browser_script_skill_tools;
use sgclaw::pipe::{Action, CommandOutput, ExecutionSurfaceKind, ExecutionSurfaceMetadata};
use sgclaw::security::MacPolicy;
use zeroclaw::skills::{Skill, SkillTool};
fn backend_policy() -> MacPolicy {
MacPolicy::from_json_str(
r#"{
"version": "1.0",
"domains": { "allowed": ["oa.example.com", "erp.example.com"] },
"pipe_actions": {
"allowed": ["click", "type", "navigate", "getText"],
"blocked": ["eval", "executeJsInPage"]
}
}"#,
)
.unwrap()
}
fn eval_policy() -> MacPolicy {
MacPolicy::from_json_str(
r#"{
"version": "1.0",
"domains": { "allowed": ["www.zhihu.com"] },
"pipe_actions": {
"allowed": ["click", "type", "navigate", "getText", "eval"],
"blocked": []
}
}"#,
)
.unwrap()
}
#[test]
fn pipe_browser_backend_keeps_privileged_pipe_surface_metadata() {
let transport = Arc::new(MockTransport::new(vec![]));
let backend = PipeBrowserBackend::new(transport, backend_policy(), vec![1, 2, 3, 4]);
let metadata = backend.surface_metadata();
assert_eq!(metadata.kind, ExecutionSurfaceKind::PrivilegedBrowserPipe);
assert!(metadata.privileged);
assert!(!metadata.defines_runtime_identity);
assert_eq!(metadata.guard, "mac_policy");
assert_eq!(
metadata.allowed_domains,
vec!["oa.example.com", "erp.example.com"]
);
assert_eq!(
metadata.allowed_actions,
vec!["click", "type", "navigate", "getText"]
);
}
#[test]
fn pipe_browser_backend_reports_eval_capability_from_mac_policy() {
let transport = Arc::new(MockTransport::new(vec![]));
let backend = PipeBrowserBackend::new(transport, eval_policy(), vec![1, 2, 3, 4]);
assert!(backend.supports_eval());
}
#[test]
fn browser_script_tools_are_hidden_when_backend_cannot_eval() {
let skill_root = unique_temp_dir("sgclaw-browser-backend-capability");
let scripts_dir = skill_root.join("scripts");
fs::create_dir_all(&scripts_dir).unwrap();
fs::write(
scripts_dir.join("extract_hotlist.js"),
"return { rows: [[1, '标题', '10万热度']] };",
)
.unwrap();
let skills = vec![Skill {
name: "zhihu-hotlist".to_string(),
description: "Zhihu hotlist helpers".to_string(),
version: "1.0.0".to_string(),
author: None,
tags: vec![],
tools: vec![SkillTool {
name: "extract_hotlist".to_string(),
description: "Extract structured hotlist rows".to_string(),
kind: "browser_script".to_string(),
command: "scripts/extract_hotlist.js".to_string(),
args: HashMap::new(),
}],
prompts: vec![],
location: Some(skill_root.join("skill.json")),
}];
let backend: Arc<dyn BrowserBackend> = Arc::new(FakeBrowserBackend::new(false));
let tools = build_browser_script_skill_tools(&skills, backend).unwrap();
assert!(tools.is_empty());
}
#[derive(Default)]
struct FakeBrowserBackend {
supports_eval: bool,
}
impl FakeBrowserBackend {
fn new(supports_eval: bool) -> Self {
Self { supports_eval }
}
}
impl BrowserBackend for FakeBrowserBackend {
fn invoke(
&self,
_action: Action,
_params: serde_json::Value,
_expected_domain: &str,
) -> Result<CommandOutput, sgclaw::pipe::PipeError> {
panic!("invoke should not be called in this capability-gating test")
}
fn surface_metadata(&self) -> ExecutionSurfaceMetadata {
ExecutionSurfaceMetadata::privileged_browser_pipe("fake_backend")
}
fn supports_eval(&self) -> bool {
self.supports_eval
}
}
fn unique_temp_dir(prefix: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!("{prefix}-{nanos}"));
fs::create_dir_all(&path).unwrap();
path
}