From 4d1070dff0d1ac9f38136f0e7f06a9d6f6e0e463 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 17:13:53 +0800 Subject: [PATCH] docs: add expected_domain arg fix 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 ------------------ .../2026-04-13-expected-domain-arg-fix.md | 55 +++++ 2 files changed, 55 insertions(+), 228 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-13-async-browser-script-support.md create mode 100644 docs/superpowers/specs/2026-04-13-expected-domain-arg-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 deleted file mode 100644 index 543016d..0000000 --- a/docs/superpowers/plans/2026-04-13-async-browser-script-support.md +++ /dev/null @@ -1,228 +0,0 @@ -# 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-expected-domain-arg-fix.md b/docs/superpowers/specs/2026-04-13-expected-domain-arg-fix.md new file mode 100644 index 0000000..cb9fc18 --- /dev/null +++ b/docs/superpowers/specs/2026-04-13-expected-domain-arg-fix.md @@ -0,0 +1,55 @@ +# 修复 Browser Script Skill Tool expected_domain 参数丢失问题 + +## 问题描述 + +`tq-lineloss-report.collect_lineloss` skill 执行时返回 `status=blocked row=0 reasons=missing_expected_domain` 错误。 + +## 根本原因 + +`src/compat/browser_script_skill_tool.rs` 中 `execute_browser_script_impl` 函数: + +```rust +// 第 183 行:从 args 中移除 expected_domain +let raw_expected_domain = match args.remove("expected_domain") { + Some(Value::String(value)) if !value.trim().is_empty() => value, + // ... +}; + +// 第 200 行:规范化域名(去掉 scheme、port 等) +let expected_domain = match normalize_domain_like(&raw_expected_domain) { + Some(value) => value, + // ... +}; + +// 第 234 行:包装脚本时,args 中已经没有 expected_domain 了! +let wrapped_script = wrap_browser_script(&script_body, &Value::Object(args.clone())); +``` + +`args.remove()` 会从 HashMap 中删除键值对,后续 `wrap_browser_script()` 传入的 args 不包含 `expected_domain`,导致 JS 脚本中 `const args = {...}` 缺少该字段。 + +## 解决方案 + +在规范化域名后,将 `expected_domain` 重新插入 args。 + +### 修改位置 + +文件:`src/compat/browser_script_skill_tool.rs` +行号:第 209 行后(`expected_domain` 赋值之后、`for required_arg` 循环之前) + +### 修改内容 + +```rust +// 第 209 行后添加: +args.insert("expected_domain".to_string(), Value::String(expected_domain.clone())); +``` + +## 影响范围 + +- 只影响 `browser_script_skill_tool.rs` +- 所有使用 `expected_domain` 的 browser_script skill 都会受益 +- 无破坏性变更 + +## 验证方法 + +1. 运行现有测试:`cargo test browser_script_skill_tool` +2. 内网验证:执行 `tq-lineloss-report.collect_lineloss` skill