From 583bb117cb543251a1cd51411e15670954fb3f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E7=82=8E?= <635735027@qq.com> Date: Mon, 13 Apr 2026 18:32:05 +0800 Subject: [PATCH] docs: add async eval .then() fix design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Qoder][https://qoder.com] --- ...2026-04-13-async-browser-script-support.md | 228 ++++++++++++++++++ .../specs/2026-04-13-async-eval-then-fix.md | 47 ++++ 2 files changed, 275 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-13-async-browser-script-support.md create mode 100644 docs/superpowers/specs/2026-04-13-async-eval-then-fix.md diff --git a/docs/superpowers/plans/2026-04-13-async-browser-script-support.md b/docs/superpowers/plans/2026-04-13-async-browser-script-support.md new file mode 100644 index 0000000..543016d --- /dev/null +++ b/docs/superpowers/plans/2026-04-13-async-browser-script-support.md @@ -0,0 +1,228 @@ +# 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] 类型一致性: 函数签名无变化 diff --git a/docs/superpowers/specs/2026-04-13-async-eval-then-fix.md b/docs/superpowers/specs/2026-04-13-async-eval-then-fix.md new file mode 100644 index 0000000..d8269dc --- /dev/null +++ b/docs/superpowers/specs/2026-04-13-async-eval-then-fix.md @@ -0,0 +1,47 @@ +# 修复 build_eval_js 异步支持 + validatePageContext 诊断日志 + +## 问题描述 + +1. `collect_lineloss.js` 的 `buildBrowserEntrypointResult` 是 async 函数,返回 Promise +2. 当前同步版 `build_eval_js` 中 `JSON.stringify(Promise)` = `"{}"` +3. 之前的 async IIFE 方案导致 `page_context_unavailable`(原因待排查) + +## 方案 + +### 修改1: build_eval_js 使用 .then() 分支 + +文件:`src/browser/callback_backend.rs` - `build_eval_js` 函数 + +逻辑: +1. 外层 IIFE 保持同步(兼容 C++ 注入层) +2. 将回调发送逻辑提取为 `_s` 函数 +3. 如果返回值是 Promise(有 `.then` 方法),用 `.then(_s)` 异步等待结果 +4. 否则直接同步调用 `_s(v)` + +```javascript +(function(){try{ + var v=(function(){return {script}})(); + function _s(v){ + var t=(typeof v==='string')?v:JSON.stringify(v); + try{callBackJsToCpp(...);}catch(_){} + var j=JSON.stringify({...}); + try{XHR...}catch(_){} + try{sendBeacon...}catch(_){} + } + if(v&&typeof v.then==='function'){v.then(_s).catch(function(){});} + else{_s(v);} +}catch(e){}})() +``` + +### 修改2: validatePageContext 添加诊断日志 + +文件:`D:\data\ideaSpace\rust\sgClaw\claw\claw\skills\skill_staging\skills\tq-lineloss-report\scripts\collect_lineloss.js` + +在 `validatePageContext` 每个检查点添加 console.log,记录 host、expected_domain、mac 状态。 + +## 验证 + +1. `cargo test` 通过 +2. 编译后拷贝 exe 到线上 +3. 执行 skill,确认不再返回 `{}` +4. 如果出现 `page_context_unavailable`,查看浏览器控制台日志