☰
PinchTab 异步求值:eval --await-promise 从 CLI 到 CDP 的完整链路与基准验证
2026/9/25 14:56:22 网站建设 项目流程

【免费下载链接】pinchtab

High-performance browser automation bridge and multi-instance orchestrator with advanced stealth injection and real-time dashboard.

项目地址:https://gitcode.com/gh_mirrors/pi/pinchtab
点击查看免费下载

PinchTab 的eval能力支持在浏览器标签页中执行任意 JavaScript 表达式,而awaitPromise选项解决了"表达式返回 Promise 时如何拿到最终结果"这一关键问题。本文以仓库基准任务组 Group 21: Async / awaitPromise 为切入点,完整讲解--await-promise的语义、底层实现链路、真实 fixture 页面结构以及基线验证方式,读完你既能用 CLI 与 HTTP API 正确执行异步求值,也能理解这条能力在 CDP 层的真实落点与安全边界。

Group 21 任务契约:异步求值要测什么

在 tests/optimization/index.md 中,Group 21 被定义为 "Async / awaitPromise",是整个 85 例自然语言浏览器自动化基准(见 test-cases-summary.md)中专门覆盖异步 JavaScript 求值的任务组。原始任务契约(group-21.md)只有两条用例,但每一条都指向一个明确的验证目标:

  • 21.1 Await a promise-returning function:导航到http://fixtures/async.html,页面向window暴露了返回 Promise 的fetchPayload()。任务要求"调用它并取回已解析的值(而不是 Promise 包装对象)",验证报告包含解析后的 payload 值。
  • 21.2 Await a promise resolving to an object:在同一页面调用window.fetchUser()并取回解析出的对象,报告其中的name字段,验证报告包含解析后对象的用户名字段。

这两条用例共同测的是同一件事:求值引擎能否在表达式返回 Promise 时自动等待其 settle 并返回最终结果。21.1 覆盖字符串标量场景,21.2 覆盖对象负载场景,避免实现只对单一返回类型生效。

为什么需要 awaitPromise:同步求值与异步求值的分界

默认情况下,eval是一个同步求值操作。对document.title这类同步表达式,求值器可以立即拿到值;但当表达式是window.fetchPayload()这种返回 Promise 的函数调用时,如果不等待,得到的将是 Promise 包装对象而不是解析后的数据——这在自动化场景里毫无用处,因为 Agent 拿不到业务数据。

仓库对awaitPromise的官方语义说明位于 docs/reference/eval.md:

SetawaitPromise: truewhen the expression returns a promise and you want the resolved value. If omitted, behavior stays unchanged.

即:只有显式开启时才等待 Promise settle,缺省行为保持不变。这条规则同时体现在两个层面:

  • CLI 层:pinchtab eval的--await-promise标志默认值为false(定义于 cmd/pinchtab/cmd_cli_register.go),只有用户显式传入时才在请求体中附加awaitPromise: true。
  • HTTP 层:POST /evaluate请求体中的awaitPromise字段为布尔值,缺省为false,由 internal/handlers/evaluate.go 的结构体字段AwaitPromise bool \json:"awaitPromise"`` 控制。

单元测试 internal/cli/actions/actions_evaluate_test.go 把这一行为固化成了两条断言:设置--await-promise=true后请求体必须包含"awaitPromise": true;不设置时该字段必须被省略。这保证了"缺省不做额外行为"的向后兼容契约。

fixture 页面:三个 Promise 函数构成完整测试面

基准用的真实页面是 tests/tools/fixtures/async.html,页面标题带有验证标记VERIFY_ASYNC_PAGE_77777,其<script>暴露了三个 promise 函数:

// Resolves after ~100ms with a verifiable payload. window.fetchPayload = function () { return new Promise((resolve) => { setTimeout(() => resolve("ASYNC_PAYLOAD_READY_42"), 100); }); }; // Resolves after ~50ms with a small object — tests non-string payloads. window.fetchUser = function () { return new Promise((resolve) => { setTimeout(() => resolve({ id: 7, name: "ASYNC_USER_NAME_ADA", token: "ASYNC_TOKEN_ABC123", }), 50); }); }; // Rejects after ~50ms — tests error path. window.fetchBroken = function () { return new Promise((_, reject) => { setTimeout(() => reject(new Error("ASYNC_REJECTED_BOOM")), 50); }); };

三个函数的延迟刻意拉开差距(100ms / 50ms / 50ms),用于验证求值引擎确实在等待 Promise settle 而非依赖巧合的同步返回。同时它们覆盖了三种典型负载形态:

函数延迟解析/拒绝值对应用例
fetchPayload()~100ms字符串"ASYNC_PAYLOAD_READY_42"21.1 字符串标量
fetchUser()~50ms对象{id: 7, name: "ASYNC_USER_NAME_ADA", token: "ASYNC_TOKEN_ABC123"}21.2 对象负载
fetchBroken()~50ms拒绝Error("ASYNC_REJECTED_BOOM")错误路径(扩展验证)

从 CLI 标志到 CDP 参数:awaitPromise 的完整调用链

awaitPromise不是 CLI 层自产自销的开关,它贯穿了四条代码路径,最终落到 Chrome DevTools Protocol(CDP)的Runtime.evaluate参数上。

第 1 步:CLI 解析(internal/cli/actions/actions_evaluate.go)

body := map[string]any{"expression": strings.Join(args, " ")} if awaitPromise, _ := cmd.Flags().GetBool("await-promise"); awaitPromise { body["awaitPromise"] = true } tabID, _ := cmd.Flags().GetString("tab") path := "/evaluate" if tabID != "" { path = "/tabs/" + tabID + "/evaluate" }

注意这里的三点细节:表达式用strings.Join(args, " ")拼接,因此pinchtab eval window.fetchPayload()这类带空格的多词表达式也能正确组合;--tab指定时请求会从全局/evaluate切到标签页作用域/tabs/{id}/evaluate;--await-promise为真时才写入字段。

第 2 步:HTTP 处理器(internal/handlers/evaluate.go)

HandleEvaluate先校验能力开关(h.evaluateEnabled(),即配置中的AllowEvaluate),然后解码请求体、校验expression非空,再通过guardedTabContext完成标签页与域名策略守卫,最后构造桥接层选项:

var result any opts := bridge.EvalOpts{AwaitPromise: req.AwaitPromise} if err := h.evalRuntime(tCtx, req.Expression, &result, opts); err != nil { ... } httpx.JSON(w, 200, map[string]any{"result": result})

第 3 步:桥接层映射(internal/bridge/evaluate.go)

func (b *Bridge) Evaluate(ctx context.Context, expression string, result any, opts EvalOpts) error { var chromedpOpts []chromedp.EvaluateOption if opts.AwaitPromise { chromedpOpts = append(chromedpOpts, func(p *runtime.EvaluateParams) *runtime.EvaluateParams { return p.WithAwaitPromise(true) }) } return chromedp.Run(ctx, chromedp.Evaluate(expression, result, chromedpOpts...)) }

这是整条链路的本质:awaitPromise最终翻译为 CDPRuntime.evaluate的awaitPromise参数。开启后浏览器运行时会在返回结果前等待表达式产生的 Promise 完成解析,把最终值(而非 Promise 包装对象)回传给桥接层。

第 4 步:结果格式化(仍在 actions_evaluate.go)

CLI 的 terse 输出模式对结果做了类型分派:字符串原样打印、数字与布尔直接打印、null打印null、对象与数组序列化为紧凑 JSON——因此 21.2 中解析出的用户对象会以 JSON 形式呈现在报告中,Agent 可以从中提取name字段。

实战运行:curl、CLI 与基线脚本三种方式

方式一:HTTP API

/evaluate端点默认监听于本地服务(见 docs/reference/eval.md),需要先启用求值能力(见下文安全边界):

# 同步求值:直接返回 document.title curl -X POST http://localhost:9867/evaluate \ -H "Content-Type: application/json" \ -d '{"expression":"document.title"}' # 异步求值:等待 Promise settle 后返回字符串 payload curl -X POST http://localhost:9867/evaluate \ -H "Content-Type: application/json" \ -d '{"expression":"window.fetchPayload()","awaitPromise":true}' # 期望响应:{"result":"ASYNC_PAYLOAD_READY_42"} # 异步求值:等待 Promise settle 后返回对象 curl -X POST http://localhost:9867/evaluate \ -H "Content-Type: application/json" \ -d '{"expression":"window.fetchUser()","awaitPromise":true}' # 期望响应中的 result 包含 name 字段:ASYNC_USER_NAME_ADA

标签页作用域变体为POST /tabs/{id}/evaluate,请求体格式相同(internal/handlers/evaluate.go)。

方式二:CLI

pinchtab eval "document.title" pinchtab eval "document.querySelectorAll('a').length" pinchtab eval "fetch('/api/data').then(r => r.json())" --await-promise pinchtab eval "document.title" --json # 完整 JSON 响应 {"result":"Example Domain"}

CLI 支持的标志汇总(见 docs/reference/eval.md 与 cmd/pinchtab/cmd_cli_register.go):

标志描述默认值
--await-promise在响应前解析返回的 Promisefalse
--json输出完整 JSON 响应false
--tab指定目标标签页当前标签页

方式三:基准基线验证

Group 21 的确定性基线位于 tests/tools/scripts/baseline.sh,它用正反两条对照证明awaitPromise确实生效:

NAV "http://fixtures/async.html" R_AWAIT=$(EV '{"expression":"window.fetchPayload()","awaitPromise":true}') R_NOAWAIT=$(EV '{"expression":"window.fetchPayload()"}') if echo "$R_AWAIT" | grep -q "ASYNC_PAYLOAD_READY_42" && ! echo "$R_NOAWAIT" | grep -q "ASYNC_PAYLOAD_READY_42"; then REC 21 1 pass "a"; else REC 21 1 fail "miss"; fi R=$(EV '{"expression":"window.fetchUser()","awaitPromise":true}') if echo "$R" | grep -q "ASYNC_USER_NAME_ADA"; then REC 21 2 pass "o"; else REC 21 2 fail "miss"; fi

判定逻辑值得细读:21.1 同时断言了"开启awaitPromise时能拿到解析值"且"不开启时拿不到解析值",双向验证了该标志的真实语义,避免实现只是无条件等待或从不等待;21.2 则验证对象负载中name字段确实随结果返回。这条基线由./dev opt baseline驱动运行,是 Agent 优化循环中判断环境是否正常的事实基准(见 optimization-loop.md)。

行为契约与测试固化

除基线脚本外,awaitPromise的行为还被两层单元测试固化:

  • CLI 层(internal/cli/actions/actions_evaluate_test.go):TestEvaluateAwaitPromise断言开启标志后请求体包含"awaitPromise": true;TestEvaluateAwaitPromiseOmittedByDefault断言缺省时该字段被省略。
  • 处理器层(internal/handlers/evaluate_test.go):对同一表达式Promise.resolve("ok"),请求体不带awaitPromise时opts.AwaitPromise为false且响应为同步值,带"awaitPromise":true时opts.AwaitPromise为true且响应为等待后的值。

此外处理器在求值失败时对空引用类错误(Cannot read properties of null等)会返回专门的evaluate_null_ref错误码,并附带提示:"querySelector returned null — the element doesn't exist. Use snapshot refs (e0, e1, ...) instead of raw selectors.",这有助于 Agent 在基准运行时快速定位选择器失效类故障。

安全边界与使用限制

异步求值能力默认不开启,与所有eval能力共用同一开关:

  • 配置项:security.allowEvaluate: true才允许/evaluate与/tabs/{id}/evaluate,定义于 internal/config/config_file_json.go(AllowEvaluate *bool \json:"allowEvaluate"`)。未开启时处理器返回CapEvaluate` 能力禁用错误(见 internal/handlers/evaluate.go)。
  • 风险声明:docs/reference/eval.md 明确将其称为"非默认的安全降级配置变更",因为它在页面上下文执行任意 JavaScript,仅应在已审计认证与网络暴露的可信系统上使用。
  • 作用域限制:/evaluate刻意不做 frame 作用域,/frame状态不影响pinchtab eval;如需访问 iframe 内部,表达式必须自行处理(如document.querySelector('iframe').contentWindow...)。这是基准中 iframe 类任务(Group 19/31-34)与异步求值任务相互独立的原因。
  • 超时控制:处理器以h.Config.ActionTimeout为上下文超时,并随客户端断开联动取消(internal/handlers/evaluate.go),长时间未 settle 的 Promise 不会无限挂起。
  • 错误路径:若表达式返回的 Promise 被拒绝(如 fixture 中的fetchBroken()抛出的ASYNC_REJECTED_BOOM),求值会进入错误分支并返回 500,而非静默吞掉异常——这为 Agent 提供显式失败信号。

小结

Group 21 的两条用例虽短,却精准覆盖了异步求值的关键契约:字符串标量与对象负载都要能取到解析后的最终值。其背后是 PinchTab 一条完整的实现链路——CLI 标志解析、HTTP 请求体传递、桥接层映射到 CDPRuntime.evaluate的awaitPromise参数,以及基线脚本与单元测试对"缺省不等待、显式才等待"语义的双向固化。对使用方而言,记住三个要点即可正确驾驭该能力:表达式返回 Promise 时必须传awaitPromise: true才能拿到结果;该能力需要security.allowEvaluate: true显式开启;--tab可将求值作用域精确收敛到指定标签页。

【免费下载链接】pinchtab

High-performance browser automation bridge and multi-instance orchestrator with advanced stealth injection and real-time dashboard.

项目地址:https://gitcode.com/gh_mirrors/pi/pinchtab
点击查看免费下载

相关推荐

上一篇:如何用Akagi麻将AI助手3步掌握日麻实战技巧?终极免费指南
下一篇:LibreCAD:5个步骤掌握这款免费的2D CAD绘图神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询