Files
claw/docs/superpowers/plans/2026-04-13-async-browser-script-support.md
2026-04-13 18:32:05 +08:00

229 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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::<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**
```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] 类型一致性: 函数签名无变化