docs: add expected_domain arg fix spec
🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -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::<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] 类型一致性: 函数签名无变化
|
|
||||||
55
docs/superpowers/specs/2026-04-13-expected-domain-arg-fix.md
Normal file
55
docs/superpowers/specs/2026-04-13-expected-domain-arg-fix.md
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user