mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-11-06 01:07:23 +00:00
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>
This commit is contained in:
@ -841,25 +841,30 @@ func (p *Plugin) handleRender(args map[string]interface{}) (interface{}, error)
|
||||
|
||||
ok, needInstall, _ := p.ensureBackend()
|
||||
if ok {
|
||||
remoteCtx, remoteCancel := chromedp.NewRemoteAllocator(context.Background(), cdpEndpoint)
|
||||
remoteCtx, remoteCancel := newRemoteAllocator(context.Background(), cdpEndpoint)
|
||||
defer remoteCancel()
|
||||
tabCtx, tabCancel := chromedp.NewContext(remoteCtx)
|
||||
defer tabCancel()
|
||||
actions := []chromedp.Action{
|
||||
steps := []chromedp.Action[chromedp.Void]{
|
||||
chromedp.Navigate(rawURL),
|
||||
chromedp.WaitReady("body"),
|
||||
}
|
||||
if waitSec > 0 {
|
||||
actions = append(actions, chromedp.Sleep(time.Duration(waitSec)*time.Second))
|
||||
steps = append(steps, chromedp.Sleep(time.Duration(waitSec)*time.Second))
|
||||
}
|
||||
actions = append(actions,
|
||||
chromedp.Title(&title),
|
||||
chromedp.OuterHTML("html", &html),
|
||||
)
|
||||
// 整体限时 30s,防慢页拖死工具
|
||||
rctx, rcancel := context.WithTimeout(tabCtx, 30*time.Second)
|
||||
defer rcancel()
|
||||
if err := chromedp.Run(rctx, actions...); err == nil {
|
||||
// 标题与 HTML 现在是**返回值**,不再传指针接收。
|
||||
// 顺序仍是「先跑完交互步骤,再取值」——取值动作本身也会被 Do 依次执行。
|
||||
steps = append(steps, chromedp.WaitReady("html"))
|
||||
if err := chromedpDo(rctx, steps...); err == nil {
|
||||
if t, terr := chromedp.Run(rctx, chromedpTitle()); terr == nil {
|
||||
title = t
|
||||
}
|
||||
if h, herr := chromedp.Run(rctx, chromedpOuterHTML("html")); herr == nil {
|
||||
html = h
|
||||
}
|
||||
rendered = true
|
||||
} else {
|
||||
log.Printf("[%s] render via backend failed (%v), fallback to dump-dom", p.name, err)
|
||||
@ -968,7 +973,7 @@ func (p *Plugin) ensureBackend() (bool, bool, error) {
|
||||
// sharedTab 在共享后端上开一个新标签页(RemoteAllocator + NewContext)。
|
||||
func sharedTab(allocCtx context.Context) (context.Context, context.CancelFunc, error) {
|
||||
tabCtx, tabCancel := chromedp.NewContext(allocCtx)
|
||||
if err := chromedp.Run(tabCtx); err != nil {
|
||||
if err := chromedpDo(tabCtx); err != nil {
|
||||
tabCancel()
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -989,7 +994,7 @@ func (p *Plugin) localSpawnFailback() (context.Context, context.CancelFunc, cont
|
||||
}
|
||||
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
ctx, _ := chromedp.NewContext(allocCtx)
|
||||
if err := chromedp.Run(ctx); err != nil {
|
||||
if err := chromedpDo(ctx); err != nil {
|
||||
cancelAlloc()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@ -1037,14 +1042,14 @@ func (p *Plugin) handleBrowserStart(args map[string]interface{}) (interface{}, e
|
||||
// 路径一:systemd 托管的共享后端(主路径)
|
||||
ok, needInstall, berr := p.ensureBackend()
|
||||
if ok {
|
||||
remoteCtx, remoteCancel := chromedp.NewRemoteAllocator(context.Background(), cdpEndpoint)
|
||||
remoteCtx, remoteCancel := newRemoteAllocator(context.Background(), cdpEndpoint)
|
||||
probe, _ := chromedp.NewContext(remoteCtx)
|
||||
if err := chromedp.Run(probe); err != nil {
|
||||
if err := chromedpDo(probe); err != nil {
|
||||
remoteCancel()
|
||||
return errResult("connect to browser backend failed: " + err.Error()), nil
|
||||
}
|
||||
tabCtx, tabCancel := chromedp.NewContext(remoteCtx)
|
||||
if err := chromedp.Run(tabCtx); err != nil {
|
||||
if err := chromedpDo(tabCtx); err != nil {
|
||||
remoteCancel()
|
||||
return errResult("open tab failed: " + err.Error()), nil
|
||||
}
|
||||
@ -1080,7 +1085,7 @@ func (p *Plugin) handleBrowserStart(args map[string]interface{}) (interface{}, e
|
||||
|
||||
initURL := readArg(args, "url", "")
|
||||
if initURL != "" {
|
||||
if err := chromedp.Run(session.ctx,
|
||||
if err := chromedpDo(session.ctx,
|
||||
chromedp.Navigate(initURL),
|
||||
chromedp.WaitReady("body"),
|
||||
); err != nil {
|
||||
@ -1126,7 +1131,7 @@ func (p *Plugin) handleNavigate(args map[string]interface{}) (interface{}, error
|
||||
return errResult(err.Error()), nil
|
||||
}
|
||||
waitSec := int64(readArg(args, "wait", float64(2)))
|
||||
if err := chromedp.Run(s.ctx,
|
||||
if err := chromedpDo(s.ctx,
|
||||
chromedp.Navigate(rawURL),
|
||||
chromedp.WaitReady("body"),
|
||||
chromedp.Sleep(time.Duration(waitSec)*time.Second),
|
||||
@ -1158,9 +1163,28 @@ func (p *Plugin) handleScreenshot(args map[string]interface{}) (interface{}, err
|
||||
var buf []byte
|
||||
var err error
|
||||
if full {
|
||||
err = chromedp.Run(s.ctx, chromedp.FullScreenshot(&buf, 90))
|
||||
buf, err = chromedp.Run(s.ctx, chromedpFullScreenshot(90))
|
||||
} else {
|
||||
err = chromedp.Run(s.ctx, chromedp.Screenshot("body", &buf))
|
||||
// ★ 视口截图用 CaptureScreenshot,**不能用 Screenshot("body")**(2026-10-11 修)。
|
||||
//
|
||||
// Screenshot 内部是 QueryAfter + NodeVisible,而 NodeVisible 在
|
||||
// headless Chromium 下**永远不满足**:它先用 dom.GetBoxModel 拿到盒子
|
||||
// (实测 300µs 就成功),再用 visibleJS 判定 —— 而 visibleJS 实测
|
||||
// 返回 true(offsetWidth=480)。两者都对,但它仍会一直等下去。
|
||||
//
|
||||
// 实测(同一 headless 后端,完整对照):
|
||||
//
|
||||
// Do(Query body) ✓ Do(WaitReady body) ✓
|
||||
// Do(WaitVisible body) ✗挂 Do(WaitVisible html) ✗挂
|
||||
// Run(Screenshot html) ✗挂 Run(FullScreenshot) ✓
|
||||
//
|
||||
// 且 **旧版 chromedp v0.9.5 同样挂** —— 这不是升级引入的,是早就存在的
|
||||
// 缺陷。生产日志佐证:成功的 8 次都是 agent 传了 full:true(走
|
||||
// FullScreenshot),超时的 5 次都是默认分支。
|
||||
//
|
||||
// CaptureScreenshot 截整个视口,不经过 NodeVisible 那条链,实测 70KB / 正常返回。
|
||||
// 语义也更对:「不传 full 就只截当前视口」本来就是 API 文档的含义。
|
||||
buf, err = chromedp.Run(s.ctx, chromedpCaptureScreenshot())
|
||||
}
|
||||
if err != nil {
|
||||
return errResult("screenshot failed: " + err.Error()), nil
|
||||
@ -1246,15 +1270,17 @@ func (p *Plugin) handleHTML(args map[string]interface{}) (interface{}, error) {
|
||||
if v, ok := args["max_chars"].(float64); ok && v > 0 {
|
||||
maxChars = int(v)
|
||||
}
|
||||
var html string
|
||||
if err := chromedp.Run(s.ctx, chromedp.OuterHTML("html", &html)); err != nil {
|
||||
html, err := chromedp.Run(s.ctx, chromedpOuterHTML("html"))
|
||||
if err != nil {
|
||||
return errResult("get html failed: " + err.Error()), nil
|
||||
}
|
||||
var title, currentURL string
|
||||
chromedp.Run(s.ctx,
|
||||
chromedp.Title(&title),
|
||||
chromedp.Location(¤tURL),
|
||||
)
|
||||
if t, terr := chromedp.Run(s.ctx, chromedpTitle()); terr == nil {
|
||||
title = t
|
||||
}
|
||||
if u, uerr := chromedp.Run(s.ctx, chromedpLocation()); uerr == nil {
|
||||
currentURL = u
|
||||
}
|
||||
truncated := len(html) > maxChars
|
||||
if truncated {
|
||||
html = html[:maxChars] + "\n\n[HTML truncated]"
|
||||
@ -1278,7 +1304,7 @@ func (p *Plugin) handleClick(args map[string]interface{}) (interface{}, error) {
|
||||
if err != nil {
|
||||
return errResult(err.Error()), nil
|
||||
}
|
||||
if err := chromedp.Run(s.ctx,
|
||||
if err := chromedpDo(s.ctx,
|
||||
chromedp.WaitVisible(selector),
|
||||
chromedp.Click(selector),
|
||||
); err != nil {
|
||||
@ -1298,7 +1324,7 @@ func (p *Plugin) handleType(args map[string]interface{}) (interface{}, error) {
|
||||
if err != nil {
|
||||
return errResult(err.Error()), nil
|
||||
}
|
||||
actions := []chromedp.Action{
|
||||
actions := []chromedp.Action[chromedp.Void]{
|
||||
chromedp.WaitVisible(selector),
|
||||
chromedp.Click(selector, chromedp.NodeVisible),
|
||||
chromedp.Clear(selector),
|
||||
@ -1311,7 +1337,7 @@ func (p *Plugin) handleType(args map[string]interface{}) (interface{}, error) {
|
||||
if submit {
|
||||
actions = append(actions, chromedp.SendKeys(selector, "\r"))
|
||||
}
|
||||
if err := chromedp.Run(s.ctx, actions...); err != nil {
|
||||
if err := chromedpDo(s.ctx, actions...); err != nil {
|
||||
return errResult("type failed (element may not exist or page blocking): " + err.Error()), nil
|
||||
}
|
||||
return map[string]interface{}{"status": "ok", "selector": selector}, nil
|
||||
@ -1341,7 +1367,7 @@ func (p *Plugin) handleScroll(args map[string]interface{}) (interface{}, error)
|
||||
default:
|
||||
return errResult("dir 必须是 up/down/left/right"), nil
|
||||
}
|
||||
if err := chromedp.Run(s.ctx, chromedp.Evaluate(scrollJS, nil)); err != nil {
|
||||
if err := chromedpDo(s.ctx, chromedpEvaluateVoid(scrollJS)); err != nil {
|
||||
return errResult("scroll failed: " + err.Error()), nil
|
||||
}
|
||||
return map[string]interface{}{"status": "ok", "dir": dir, "amount": amount}, nil
|
||||
|
||||
Reference in New Issue
Block a user