Files
homeagent-sdk/example/browser/chromedp_compat.go
JianFeeeee 0b497f4876 fix(browser)!: 升级 chromedp v0.9.5→v0.20.1,修视口截图必挂 (#1)
用户报告截图不对(此前已改为落盘返回路径)。查下去发现**两个叠加的问题**,
其中一个是既有的、与上次改动无关的严重缺陷。

## ① 协议解析失败:chromedp v0.9.5 不认 Chrome 154

```
ERROR: could not unmarshal event: parse error: expected string near offset 411 of 'cookiePart...'
```

cdproto 把 `partitionKey` 定义为 `string`,而 Chrome 154 已改成对象
(`cookiePartitionKey`)。新版 cdproto 定义了 `CookiePartitionKey` 结构体,
且 `UnmarshalJSON` **双向兼容**(字符串 → 结构体,旧浏览器也不坏)。

副作用:`Navigate` 从 2.5s 降到 85ms(解析错误消失,不再反复重试)。

## ② 视口截图在 headless 下**永远挂住**(既有缺陷,不是升级引入)

`Screenshot(sel)` 内部是 `QueryAfter` + `NodeVisible`,而 NodeVisible 在
headless Chromium 上永远不满足 —— 两步单独看都成功,但它一直等下去:

```
Do(Query body)          ✓        Do(WaitReady body)      ✓
Do(WaitVisible body)    ✗挂住     Do(WaitVisible html)    ✗挂住
Run(Screenshot html)    ✗挂住     Run(FullScreenshot)     ✓
Run(CaptureScreenshot)  ✓(70KB)
```

**决定性对照:旧版 chromedp v0.9.5 在同一个后端上同样挂。**
所以这不是升级引入的。生产日志也佐证:

- 成功的 8 次 —— agent 都传了 `full:true`(走 FullScreenshot)
- 超时的 5 次 —— 都是默认分支(走 Screenshot("body"))

且 `NodeVisible` 的两步实测都正常(`dom.GetBoxModel` 300µs 成功、
`visibleJS` 返回 true / offsetWidth=480),就是等待机制本身不返回。
没有深挖到 chromedp 内部的根因,但**换用 `CaptureScreenshot` 完全绕开这条链**,
且语义更对:「不传 full 就只截当前视口」本来就是 API 文档的含义。

## 迁移方式:兼容层收敛,业务代码最小改动

v0.20 是**泛型重写**(`Action[T]` 返回值而不填指针),25 处调用点受影响。
新增 `chromedp_compat.go` 把新 API 收敛成几个小助手
(`chromedpDo` / `newRemoteAllocator` / `chromedpCaptureScreenshot` …),
业务代码保持旧写法。

为何不逐处改:20+ 处机械改写会把 diff 淹没,真正要审的
「行为变了没有」反而看不清。

`NewRemoteAllocator` 在 v0.20 移到了独立子模块 `remote`(核心模块现在
只用标准库 + cdproto,需要 websocket 的东西全部搬出去)。

## 顺带修掉 go.mod 的重复 replace

`go mod tidy` 清掉了 4 条重复的 `replace ... => ../../`(hmapdev 往已有
go.mod 追加却不去重导致的)。Go 容忍它,但 `go mod tidy` 会报错。

## 判据(可杀变异,实测)

`chromedp_compat_test.go`:
- 视口截图必须走 `CaptureScreenshot`,不得退回 `Screenshot(sel)`
  (改回挂住的写法 → FAIL ×2)
- 兼容层关键助手都在(去掉 CaptureScreenshot → build FAIL)
- 无旧式「传指针接收截图」残留

全部 20 个 example `go vet` 通过;SDK 根 module build/test 通过。
生产实测:agent 做视口截图返回
`/home/newqqagent/browser_screenshots/shot-20261011-104225.610-browser_1.png`
(1280×713 PNG / 70518 字节),不再超时。

Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
2026-10-11 10:45:41 +08:00

72 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"github.com/chromedp/chromedp"
"github.com/chromedp/chromedp/remote"
)
// chromedp v0.20 是**泛型重写**(Action[T] 返回值而不填指针),
// 本文件把旧写法收敛成几个小助手,让业务代码的改动面最小。
//
// 为何不直接在每个调用点改:插件里有 20+ 处 `chromedp.Run(ctx, 步骤...)`,
// 逐处改成「Run 取一个值 / Do 跑多步」会让 diff 淹没在机械改写里,
// 真正要审的「行为变了没有」反而看不清。收敛到助手后,业务代码保持原样。
// chromedpDo 依次跑多个不返回值的步骤(旧 Run(ctx, steps...) 的等价物)。
//
// 对应 v0.20 的 chromedp.Do。放在这里是为了让调用点看起来与旧代码一致。
func chromedpDo(ctx context.Context, steps ...chromedp.Action[chromedp.Void]) error {
return chromedp.Do(ctx, steps...)
}
// newRemoteAllocator 连接一个已在运行的 CDP 端点(如共享浏览器后端)。
//
// v0.20 把它移到了独立子模块:chromedp.NewRemoteAllocator → remote.NewAllocator。
// 子模块存在的理由:核心模块现在只用标准库 + cdproto,需要 websocket 的东西
// 全部搬出去(见 chromedp docs/decisions/2026-10-04-...)。
func newRemoteAllocator(parent context.Context, url string) (context.Context, context.CancelFunc) {
return remote.NewAllocator(parent, url)
}
// chromedpTitle 取当前页标题。
func chromedpTitle(sel ...chromedp.QueryOption) chromedp.Action[string] {
return chromedp.Title()
}
// chromedpLocation 取当前页 URL。
func chromedpLocation() chromedp.Action[string] {
return chromedp.Location()
}
// chromedpOuterHTML 取元素的外部 HTML(旧写法传 &html 接收,现在直接返回)。
func chromedpOuterHTML(sel string, opts ...chromedp.QueryOption) chromedp.Action[string] {
return chromedp.OuterHTML(sel, opts...)
}
// chromedpScreenshot 截图(旧写法传 &buf 接收,现在直接返回)。
func chromedpScreenshot(sel string, opts ...chromedp.QueryOption) chromedp.Action[[]byte] {
return chromedp.Screenshot(sel, opts...)
}
// chromedpFullScreenshot 整页截图。
func chromedpFullScreenshot(quality int) chromedp.Action[[]byte] {
return chromedp.FullScreenshot(quality)
}
// chromedpCaptureScreenshot 截当前**视口**。
//
// ★ 为何不用 chromedp.Screenshot("body")(2026-10-11):后者内部是
// QueryAfter + NodeVisible,而 NodeVisible 在本机的 headless Chromium 上
// 永远不会满足 —— 实测会一直挂到调用方超时(生产表现为 60s 工具超时)。
// 详见 plugin.go 里 handleScreenshot 的注释(含完整对照表)。
func chromedpCaptureScreenshot() chromedp.Action[[]byte] {
return chromedp.CaptureScreenshot()
}
// chromedpEvaluate 执行 JS(返回 Void 表示不关心返回值)。
func chromedpEvaluateVoid(expr string) chromedp.Action[chromedp.Void] {
return chromedp.Evaluate[chromedp.Void](expr, nil)
}