mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 14:27:30 +00:00
用户报告截图不对(此前已改为落盘返回路径)。查下去发现**两个叠加的问题**,
其中一个是既有的、与上次改动无关的严重缺陷。
## ① 协议解析失败: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>
108 lines
3.9 KiB
Go
108 lines
3.9 KiB
Go
package main
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 视口截图**必须**走 CaptureScreenshot,不能用 Screenshot(sel)。
|
||
//
|
||
// # 为什么这条判据存在(2026-10-11 实测)
|
||
//
|
||
// chromedp 的 Screenshot(sel) 内部是 QueryAfter + **NodeVisible**,
|
||
// 而 NodeVisible 在本机的 headless Chromium 上**永远不会满足** —— 它先
|
||
// 用 dom.GetBoxModel 拿盒子(实测 300µs 就成功),再用 visibleJS 判定
|
||
// (实测返回 true,offsetWidth=480),两步都对,但它仍一直等下去。
|
||
//
|
||
// 同一后端上的完整对照(实测):
|
||
//
|
||
// Do(Query body) ✓ Do(WaitReady body) ✓
|
||
// Do(WaitVisible body) ✗挂住 Do(WaitVisible html) ✗挂住
|
||
// Run(Screenshot html) ✗挂住 Run(FullScreenshot) ✓
|
||
// Run(CaptureScreenshot) ✓
|
||
//
|
||
// 且**旧版 chromedp v0.9.5 同样挂** —— 这不是升级引入的,是既有缺陷。
|
||
// 生产日志佐证:成功的都是 agent 传了 full:true(走 FullScreenshot),
|
||
// 超时的都是默认分支(走 Screenshot("body"))。
|
||
//
|
||
// 所以这条判据守的是:不许把 CaptureScreenshot 改回 Screenshot(sel)。
|
||
func TestViewportScreenshotUsesCaptureScreenshot(t *testing.T) {
|
||
src, err := os.ReadFile("plugin.go")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
|
||
i := strings.Index(s, "func (p *Plugin) handleScreenshot(")
|
||
if i < 0 {
|
||
t.Fatal("找不到 handleScreenshot")
|
||
}
|
||
seg := s[i:]
|
||
if j := strings.Index(seg, "\nfunc (p *Plugin) saveScreenshot("); j > 0 {
|
||
seg = seg[:j]
|
||
}
|
||
|
||
// ① 视口分支必须用 CaptureScreenshot
|
||
if !strings.Contains(seg, "chromedpCaptureScreenshot()") {
|
||
t.Error("视口截图必须走 chromedpCaptureScreenshot()(=CaptureScreenshot):\n" +
|
||
" 用 Screenshot(\"body\") 会因 NodeVisible 在 headless 下永不满足而挂到超时")
|
||
}
|
||
|
||
// ② 不得再出现挂住的那种写法
|
||
if strings.Contains(seg, `chromedpScreenshot(`) {
|
||
t.Error("handleScreenshot 里出现了 chromedpScreenshot(sel) —— " +
|
||
"该动作在 headless 下必挂(实测),改用 chromedpCaptureScreenshot()")
|
||
}
|
||
|
||
// ③ full 分支仍走 FullScreenshot(那条一直是好的)
|
||
if !strings.Contains(seg, "chromedpFullScreenshot(") {
|
||
t.Error("full=true 应走 chromedpFullScreenshot(FullScreenshot 实测正常)")
|
||
}
|
||
}
|
||
|
||
// TestCompatLayerExists 确认兼容层还在(迁移收敛点)。
|
||
//
|
||
// 这一层是为了让 20+ 处调用点的迁移不淹没 diff。若有人删掉它而没把
|
||
// 调用点改对,编译会红;但若有人把它改成直接透传错误 API,就只会
|
||
// 在运行时挂住 —— 所以这里钉住关键几个。
|
||
func TestCompatLayerExists(t *testing.T) {
|
||
src, err := os.ReadFile("chromedp_compat.go")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
for _, need := range []string{
|
||
"func chromedpDo(",
|
||
"func newRemoteAllocator(",
|
||
"func chromedpCaptureScreenshot(",
|
||
"func chromedpFullScreenshot(",
|
||
"func chromedpTitle(",
|
||
} {
|
||
if !strings.Contains(s, need) {
|
||
t.Errorf("chromedp 兼容层缺少 %s", need)
|
||
}
|
||
}
|
||
// 必须用 remote 子模块的分配器(v0.20 把它移出去了)
|
||
if !strings.Contains(s, "remote.NewAllocator") {
|
||
t.Error("newRemoteAllocator 必须用 remote.NewAllocator(v0.20 移出核心模块)")
|
||
}
|
||
}
|
||
|
||
// TestNoLegacyPointerScreenshot 确认没有残留的旧式「传指针接收」写法。
|
||
//
|
||
// v0.20 的截图动作是**返回值**,传 &buf 会直接编译失败 —— 但这条判据
|
||
// 仍值得留着:它锁的是「值语义」这个契约,而不是某次编译结果。
|
||
func TestNoLegacyPointerScreenshot(t *testing.T) {
|
||
for _, f := range []string{"plugin.go", "chromedp_compat.go"} {
|
||
b, err := os.ReadFile(f)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(b)
|
||
if strings.Contains(s, "Screenshot(&") || strings.Contains(s, "FullScreenshot(&") {
|
||
t.Errorf("%s 仍用旧式传指针接收截图(v0.20 已改为返回值)", f)
|
||
}
|
||
}
|
||
}
|