Browser4 icon indicating copy to clipboard operation
Browser4 copied to clipboard

Improved driver.evaluate()

Open platonai opened this issue 2 months ago • 1 comments

val result = driver.evaluateDetail("window.getScrollableElementXpathsSync()")的返回值是JsEvaluation(value=null, unserializableValue=null, className=Array, description=Array(1), exception=null),如何将Array对象序列化到kotlin程序中呢?你需要检查 Runtime.java 中相关方法的说明。

我看到了你拿到的返回:JsEvaluation(value=null, unserializableValue=null, className=Array, description=Array(1), exception=null)。这是因为当前用的是 driver.evaluateDetail(...),底层调用了 CDP Runtime.evaluate 的默认行为,返回的是一个“远程对象引用”(RemoteObject),对 Array 这种非原始类型不会自动把值塞到 value 字段里,而是给你 objectId/className/description 等元数据,所以 value 会是 null。 对策要点(先给结论,再解释原因): 想要把 JS Array 实体序列化成 Kotlin 可用的值,请改用 driver.evaluateValue(...) 或 driver.evaluateValueDetail(...)。它们会以 “returnByValue=true” 的方式请求 CDP,把 JSON 可序列化的对象(数组/纯对象)按值回传到 result.value。 如果你的结果包含非 JSON 可序列化的对象(比如 DOM 节点、循环引用等),则需要使用 CDP 的 serializationOptions 且设为 deep;或者在 JS 侧先 JSON.stringify 再回传字符串,最后在 Kotlin 里解析。 为什么:依据 Runtime.java 的方法说明 Runtime.evaluate 的 Javadoc 已明确: returnByValue: Whether the result is expected to be a JSON object that should be sent by value. 也就是只有设定 returnByValue 才会把对象按值放进 result.value。 serializationOptions: Specifies the result serialization. 如果提供,会覆盖 returnByValue/generatePreview/generateWebDriverValue。其枚举 SerializationOptionsSerialization 支持 deep/json/idOnly,其中 deep 可以把复杂结构深度序列化为 deepSerializedValue。 你当前的 PageHandler.evaluateDetail 是直接 runtime.evaluate(expression)(没有设置 returnByValue/serializationOptions),所以 Array 只作为远程对象返回,value=null。 可用的两种实操方案 方案 A(推荐,最少改动):使用按值返回的 API 直接拿值: val v = driver.evaluateValue("window.getScrollableElementXpathsSync()") 如果该 JS 函数返回的是字符串数组,那么 v 在 Kotlin 侧会是 List<>;你可以安全转换: val xs: List = (v as? List<>)?.map { it.toString() } ?: emptyList() 如果想拿到 JsEvaluation 结构: val detail = driver.evaluateValueDetail("window.getScrollableElementXpathsSync()") val xs: List = (detail?.value as? List<*>)?.map { it.toString() } ?: emptyList() 说明:evaluateValue / evaluateValueDetail 会通过 returnByValue=true 请求 CDP,把 JSON 序列化兼容的结果(数组/纯对象/原始类型)塞到 result.value,这正是你需要的。 方案 B(兼容复杂对象或保持 evaluateDetail):JS 侧先转 JSON 字符串,再在 Kotlin 解析 在 JS 里先 JSON.stringify: val json = driver.evaluate("JSON.stringify(window.getScrollableElementXpathsSync())") as String Kotlin 里反序列化(项目里已有 prettyPulsarObjectMapper,可直接用): val xs: List = prettyPulsarObjectMapper.readValue(json, object : com.fasterxml.jackson.core.type.TypeReference<List>() {}) 适用场景:当对象结构可能包含深层或某些 returnByValue 不能直接处理的元素时,手工 stringify 是最稳定的。 进阶:需要深度序列化(deep serialization)时 Runtime.evaluate/callFunctionOn 还支持 serializationOptions,并且 SerializationOptionsSerialization 有 DEEP/JSON/ID_ONLY: deep 会把结果放在 RemoteObject.deepSerializedValue 中(而非 value)。 当前 WebDriver API 暴露了 evaluateValueDetail 但没有显式开放传递 serializationOptions 的变体。如果你遇到非 JSON 可序列化的复杂值(例如包含 DOM 节点),可以考虑增加一个新方法(例如 evaluateDeepValueDetail)调用 PageHandler.cdpEvaluate(expression, serializationOptions = SerializationOptions().apply { serialization = DEEP; maxDepth = ... }),或者在 JS 侧转成结构化的纯 JSON 再传回。 小结与建议 针对你这个具体方法 window.getScrollableElementXpathsSync()(看名字应返回一组 XPath 字符串),使用 driver.evaluateValue(...) / driver.evaluateValueDetail(...) 最直接,Kotlin 侧按 List 取值就行。 如果必须保留 evaluateDetail(...),就用 JSON.stringify 包一层,让它返回字符串,再用 Jackson 解析成 List 这正符合 Runtime.java 的注释:默认 evaluate 返回远程对象引用;要“按值返回”,设置 returnByValue,或使用 serializationOptions(deep/json)。

platonai avatar Oct 10 '25 13:10 platonai

我看到你为了实现 generateXPathsForElementSync 方法,将一批方法改成了 sync 版本,能不能直接用 generateXPathsForElement 来实现 generateXPathsForElementSync 呢?

我看到你的问题了——你想知道能不能用异步的 generateXPathsForElement 直接实现同步的 generateXPathsForElementSync。我的结论是:不能直接这样做,但可以把「同步实现」作为「唯一事实来源」,然后让异步版本去复用同步版本,从而去重且保持一致。

计划

• 解释为什么不能从异步函数直接实现同步函数

• 给出合理的重构建议:以同步实现为核心,异步函数包装同步结果

答案与建议

🔍 为什么不行?

JavaScript 在浏览器里没有「阻塞等待 Promise 完成」的能力。换句话说:

  1. 同步函数不能 await 异步操作
  2. 也无法通过「转同步」的方式拿到异步结果
  3. 其他变通方案(如 Atomics.wait 或同步 XHR)要么不可用(需 cross-origin isolation),要么风险极高(阻塞 UI 线程),不推荐

✅ 更好的做法:「同步实现」作为单一事实来源

  1. 将已有的同步方法设为核心(如 generateComplexXPathSync、generateStandardXPathSync、generatedIdBasedXPathSync、generateXPathsForElementSync)。
  2. 异步函数仅包装同步结果,避免重复逻辑和未来不一致的风险。

示例代码:

// 原异步函数改为包装同步版本 async function generateXPathsForElement(element) { return Promise.resolve(generateXPathsForElementSync(element)); }

// 其他异步函数也同理 async function generateComplexXPath() { return Promise.resolve(generateComplexXPathSync()); }

🌟 这样做的好处

• 去重:只有一份核心逻辑(同步实现),避免维护两套代码

• 一致性:同步和异步版本的结果完全一致

• 扩展灵活:

• 若未来需要真正的异步操作(如跨文档、远程查询),只需在异步包装层添加 await 分支

• 同步 API 仍保持「仅在纯同步路径可用」,避免误用

小提示

  1. 你当前的「异步版本」其实没有真正的异步操作(仅用了 async 声明 + Promise.all 包装),改为包装同步版本后:
    • 外部行为完全不变

    • 彻底消除重复实现

  2. 反过来(用异步实现同步)在 JS 中不可行,除非引入不推荐的阻塞技巧(如 Atomics.wait),但这会带来兼容性、性能和维护性问题。

如果需要,我可以帮你对 stagehand.js 做一次小改造:
保留现有 Sync 实现为核心,将异步函数改为 Promise.resolve(...Sync()) 的轻量包装,确保:
• 对外 API 不变

• 逻辑去重

• 更易维护

platonai avatar Oct 10 '25 14:10 platonai