Files
claw/docs/superpowers/plans/2026-04-13-async-browser-script-support.md
木炎 dbbc5d030b docs: add async browser script support implementation plan
Plan for modifying build_eval_js to support async scripts.
Two tasks: modify callback_backend.rs, add test case.

🤖 Generated with [Qoder][https://qoder.com]
2026-04-13 16:09:09 +08:00

7.4 KiB
Raw Blame History

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

当前代码:

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){{}}}})()"
    )
}

修改后代码:

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
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 文件末尾添加新测试:

#[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::<serde_json::Value>(&result.output).unwrap();
    assert_eq!(output["async"], true);
}
  • Step 2: 运行新测试

Run: cargo test execute_browser_script_tool_awaits_async_script Expected: 测试通过

  • Step 3: Commit
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. 预期结果: 返回实际报表数据,而非 {}

自检清单

  • Spec 覆盖: 设计文档中所有要点都有对应任务
  • 无占位符: 所有代码都是完整的
  • 类型一致性: 函数签名无变化