# Async Browser Script 支持实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 修改 `build_eval_js` 函数支持异步脚本,解决 Promise 被 JSON.stringify 序列化为 `{}` 的问题。 **Architecture:** 将 `build_eval_js` 生成的 JavaScript 代码从同步 IIFE 改为 async IIFE,用 await 等待脚本执行结果,并检测 Promise-like 对象进行二次等待。 **Tech Stack:** Rust, JavaScript (生成代码) --- ## 文件结构 | 文件 | 操作 | 说明 | |------|------|------| | `src/browser/callback_backend.rs` | 修改 | 修改 `build_eval_js` 函数 | | `tests/browser_script_skill_tool_test.rs` | 新增测试 | 添加异步脚本测试用例 | --- ### Task 1: 修改 build_eval_js 支持异步脚本 **Files:** - Modify: `src/browser/callback_backend.rs:433-447` **当前代码:** ```rust fn build_eval_js(source_url: &str, script: &str) -> String { let escaped_source_url = escape_js_single_quoted(source_url); let callback = EVAL_CALLBACK_NAME; let events_url = escape_js_single_quoted(&events_endpoint_url(source_url)); format!( "(function(){{try{{var v=(function(){{return {script}}})();\ var t=(typeof v==='string')?v:JSON.stringify(v);\ try{{callBackJsToCpp('{escaped_source_url}@_@'+window.location.href+'@_@{callback}@_@sgBrowserExcuteJsCodeByDomain@_@'+(t??''))}}catch(_){{}}\ var j=JSON.stringify({{type:'callback',callback:'{callback}',request_url:'{escaped_source_url}',payload:{{value:(t??'')}}}});\ try{{var r=new XMLHttpRequest();r.open('POST','{events_url}',true);r.setRequestHeader('Content-Type','application/json');r.send(j)}}catch(_){{}}\ try{{navigator.sendBeacon('{events_url}',new Blob([j],{{type:'application/json'}}))}}catch(_){{}}\ }}catch(e){{}}}})()" ) } ``` **修改后代码:** ```rust fn build_eval_js(source_url: &str, script: &str) -> String { let escaped_source_url = escape_js_single_quoted(source_url); let callback = EVAL_CALLBACK_NAME; let events_url = escape_js_single_quoted(&events_endpoint_url(source_url)); format!( "(async function(){{try{{\ var v=await (async function(){{return {script}}})();\ if(v&&typeof v.then==='function'){{v=await v;}}\ var t=(typeof v==='string')?v:JSON.stringify(v);\ try{{callBackJsToCpp('{escaped_source_url}@_@'+window.location.href+'@_@{callback}@_@sgBrowserExcuteJsCodeByDomain@_@'+(t??''))}}catch(_){{}}\ var j=JSON.stringify({{type:'callback',callback:'{callback}',request_url:'{escaped_source_url}',payload:{{value:(t??'')}}}});\ try{{var r=new XMLHttpRequest();r.open('POST','{events_url}',true);r.setRequestHeader('Content-Type','application/json');r.send(j)}}catch(_){{}}\ try{{navigator.sendBeacon('{events_url}',new Blob([j],{{type:'application/json'}}))}}catch(_){{}}\ }}catch(e){{}}}})()" ) } ``` **关键变更说明:** 1. `(function()` → `(async function()` - 整个 IIFE 变为异步 2. `var v=(function(){return {script}})()` → `var v=await (async function(){return {script}})()` - 内部包装也变为异步并 await 3. 新增 `if(v&&typeof v.then==='function'){v=await v;}` - 检测并等待 Promise-like 对象 - [ ] **Step 1: 修改 build_eval_js 函数** 编辑 `src/browser/callback_backend.rs` 第 433-447 行,替换为上述新代码。 - [ ] **Step 2: 编译验证** Run: `cargo build` Expected: 编译成功,无错误 - [ ] **Step 3: 运行现有测试** Run: `cargo test browser_script_skill_tool` Expected: 所有测试通过 - [ ] **Step 4: Commit** ```bash git add src/browser/callback_backend.rs git commit -m "fix: support async browser scripts in build_eval_js Wrap eval script in async IIFE and await Promise-like results. Fixes Promise serialization returning '{}' for async skill scripts. 🤖 Generated with [Qoder][https://qoder.com]" ``` --- ### Task 2: 添加异步脚本测试用例 **Files:** - Modify: `tests/browser_script_skill_tool_test.rs` - [ ] **Step 1: 添加异步脚本测试用例** 在 `tests/browser_script_skill_tool_test.rs` 文件末尾添加新测试: ```rust #[tokio::test] async fn execute_browser_script_tool_awaits_async_script() { let skill_dir = unique_temp_dir("sgclaw-browser-script-async"); let scripts_dir = skill_dir.join("scripts"); fs::create_dir_all(&scripts_dir).unwrap(); // 异步脚本,返回 Promise fs::write( scripts_dir.join("async_extract.js"), "return (async function() { return { async: true, args: args }; })();\n", ) .unwrap(); let transport = Arc::new(MockTransport::new(vec![BrowserMessage::Response { seq: 1, success: true, data: json!({ "text": { "async": true, "args": { "expected_domain": "example.com" } } }), aom_snapshot: vec![], timing: Timing { queue_ms: 1, exec_ms: 5, }, }])); let mut policy_json = test_policy(); // 允许 example.com policy_json = MacPolicy::from_json_str( r#"{ "version": "1.0", "domains": { "allowed": ["www.zhihu.com", "example.com"] }, "pipe_actions": { "allowed": ["click", "type", "navigate", "getText", "eval"], "blocked": [] } }"#, ) .unwrap(); let browser_tool = BrowserPipeTool::new( transport.clone(), policy_json, vec![1, 2, 3, 4, 5, 6, 7, 8], ) .with_response_timeout(Duration::from_secs(1)); let skill_tool = SkillTool { name: "async_extract".to_string(), description: "Extract data asynchronously".to_string(), kind: "browser_script".to_string(), command: "scripts/async_extract.js".to_string(), args: HashMap::new(), }; let result = execute_browser_script_tool( &skill_tool, &skill_dir, &PipeBrowserBackend::from_inner(browser_tool), json!({ "expected_domain": "example.com" }), ) .await .unwrap(); assert!(result.success); let output = serde_json::from_str::(&result.output).unwrap(); assert_eq!(output["async"], true); } ``` - [ ] **Step 2: 运行新测试** Run: `cargo test execute_browser_script_tool_awaits_async_script` Expected: 测试通过 - [ ] **Step 3: Commit** ```bash git add tests/browser_script_skill_tool_test.rs git commit -m "test: add async browser script test case 🤖 Generated with [Qoder][https://qoder.com]" ``` --- ### Task 3: 端到端验证 **Files:** - 无文件修改,仅验证 - [ ] **Step 1: 完整构建** Run: `cargo build` Expected: 编译成功 - [ ] **Step 2: 运行全部测试** Run: `cargo test` Expected: 所有测试通过 - [ ] **Step 3: 手动端到端测试** 使用 service console 测试 `tq-lineloss-report.collect_lineloss`: 1. 启动 sgclaw: `target/debug/sg_claw.exe` 2. 在 service console 输入: `兰州公司 台区线损大数据 月累计线损率统计分析。。。` 3. 预期结果: 返回实际报表数据,而非 `{}` --- ## 自检清单 - [x] Spec 覆盖: 设计文档中所有要点都有对应任务 - [x] 无占位符: 所有代码都是完整的 - [x] 类型一致性: 函数签名无变化