Improved driver.evaluate()
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
我看到你为了实现 generateXPathsForElementSync 方法,将一批方法改成了 sync 版本,能不能直接用 generateXPathsForElement 来实现 generateXPathsForElementSync 呢?
我看到你的问题了——你想知道能不能用异步的 generateXPathsForElement 直接实现同步的 generateXPathsForElementSync。我的结论是:不能直接这样做,但可以把「同步实现」作为「唯一事实来源」,然后让异步版本去复用同步版本,从而去重且保持一致。
计划
• 解释为什么不能从异步函数直接实现同步函数
• 给出合理的重构建议:以同步实现为核心,异步函数包装同步结果
答案与建议
🔍 为什么不行?
JavaScript 在浏览器里没有「阻塞等待 Promise 完成」的能力。换句话说:
- 同步函数不能 await 异步操作
- 也无法通过「转同步」的方式拿到异步结果
- 其他变通方案(如 Atomics.wait 或同步 XHR)要么不可用(需 cross-origin isolation),要么风险极高(阻塞 UI 线程),不推荐
✅ 更好的做法:「同步实现」作为单一事实来源
- 将已有的同步方法设为核心(如 generateComplexXPathSync、generateStandardXPathSync、generatedIdBasedXPathSync、generateXPathsForElementSync)。
- 异步函数仅包装同步结果,避免重复逻辑和未来不一致的风险。
示例代码:
// 原异步函数改为包装同步版本 async function generateXPathsForElement(element) { return Promise.resolve(generateXPathsForElementSync(element)); }
// 其他异步函数也同理 async function generateComplexXPath() { return Promise.resolve(generateComplexXPathSync()); }
🌟 这样做的好处
• 去重:只有一份核心逻辑(同步实现),避免维护两套代码
• 一致性:同步和异步版本的结果完全一致
• 扩展灵活:
• 若未来需要真正的异步操作(如跨文档、远程查询),只需在异步包装层添加 await 分支
• 同步 API 仍保持「仅在纯同步路径可用」,避免误用
小提示
-
你当前的「异步版本」其实没有真正的异步操作(仅用了 async 声明 + Promise.all 包装),改为包装同步版本后:
• 外部行为完全不变• 彻底消除重复实现
-
反过来(用异步实现同步)在 JS 中不可行,除非引入不推荐的阻塞技巧(如 Atomics.wait),但这会带来兼容性、性能和维护性问题。
如果需要,我可以帮你对 stagehand.js 做一次小改造:
保留现有 Sync 实现为核心,将异步函数改为 Promise.resolve(...Sync()) 的轻量包装,确保:
• 对外 API 不变
• 逻辑去重
• 更易维护