Files
AutoZero/cdp/cdp.go
T
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

222 lines
5.2 KiB
Go

// Package cdp 实现跨平台 browser 工具所需的 Chrome DevTools Protocol 客户端:
// WebSocket 传输(coder/websocket)、CDP 连接层、Chrome 启动发现、browser 工具。
package cdp
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
"github.com/coder/websocket"
)
// maxWSMessage 限制单条 CDP 消息大小(截图 base64 可达数 MB,留足余量)。
const maxWSMessage = 64 << 20
// Conn 是一条到 CDP target 的 WebSocket 连接:请求/响应按 id 配对,
// 事件按方法名分发给先注册的等待者(必须先 Subscribe 再触发事件)。
type Conn struct {
ws *websocket.Conn
ctx context.Context // 连接生命周期(readLoop 用)
wmu sync.Mutex // coder/websocket 要求写方串行
mu sync.Mutex
nextID int64
pending map[int64]chan cdpReply
subs map[string][]*EventWaiter
closed bool
}
type cdpReply struct {
Result json.RawMessage
Err *cdpError
}
type cdpError struct {
Code int `json:"code"`
Message string `json:"message"`
}
type cdpEvent struct {
Method string
Params json.RawMessage
}
type cdpWireMsg struct {
ID int64 `json:"id"`
Result json.RawMessage `json:"result"`
Error *cdpError `json:"error"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
// Dial 连接一个 target 的 WebSocket 调试端点并启动读取循环。
func Dial(ctx context.Context, wsURL string) (*Conn, error) {
dctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
ws, _, err := websocket.Dial(dctx, wsURL, &websocket.DialOptions{CompressionMode: websocket.CompressionDisabled})
if err != nil {
return nil, fmt.Errorf("ws dial %s: %w", wsURL, err)
}
ws.SetReadLimit(maxWSMessage)
c := &Conn{
ws: ws,
ctx: ctx,
pending: map[int64]chan cdpReply{},
subs: map[string][]*EventWaiter{},
}
go c.readLoop()
return c, nil
}
// Call 发送一条 CDP 命令并等待同 id 响应。
func (c *Conn) Call(ctx context.Context, method string, params any) (json.RawMessage, error) {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return nil, errors.New("cdp connection closed")
}
c.nextID++
id := c.nextID
ch := make(chan cdpReply, 1)
c.pending[id] = ch
c.mu.Unlock()
msg := map[string]any{"id": id, "method": method}
if params != nil {
msg["params"] = params
}
data, err := json.Marshal(msg)
if err != nil {
c.removePending(id)
return nil, err
}
c.wmu.Lock()
err = c.ws.Write(ctx, websocket.MessageText, data)
c.wmu.Unlock()
if err != nil {
c.removePending(id)
return nil, fmt.Errorf("%s: %w", method, err)
}
select {
case r := <-ch:
if r.Err != nil {
return nil, fmt.Errorf("%s: %s (code %d)", method, r.Err.Message, r.Err.Code)
}
return r.Result, nil
case <-ctx.Done():
c.removePending(id)
return nil, ctx.Err()
}
}
// Subscribe 注册事件等待者(必须在事件触发前调用;缓冲 8,满则丢弃)。
func (c *Conn) Subscribe(event string) *EventWaiter {
w := &EventWaiter{ch: make(chan cdpEvent, 8), method: event}
c.mu.Lock()
c.subs[event] = append(c.subs[event], w)
c.mu.Unlock()
return w
}
// EventWaiter 等待一个已订阅的 CDP 事件。
type EventWaiter struct {
ch chan cdpEvent
method string
}
// Wait 等待事件到达(超时返回错误)。
func (w *EventWaiter) Wait(ctx context.Context, timeout time.Duration) (json.RawMessage, error) {
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case ev := <-w.ch:
return ev.Params, nil
case <-ctx.Done():
return nil, ctx.Err()
case <-timer.C:
return nil, fmt.Errorf("timeout waiting for event %s", w.method)
}
}
func (c *Conn) removePending(id int64) {
c.mu.Lock()
delete(c.pending, id)
c.mu.Unlock()
}
// readLoop 按 id 分发响应、按方法名分发事件;连接断开时唤醒所有等待者。
func (c *Conn) readLoop() {
for {
_, data, err := c.ws.Read(c.ctx)
if err != nil {
c.failAll(err)
return
}
var m cdpWireMsg
if err := json.Unmarshal(data, &m); err != nil {
continue
}
if m.ID != 0 {
c.mu.Lock()
ch := c.pending[m.ID]
delete(c.pending, m.ID)
c.mu.Unlock()
if ch != nil {
ch <- cdpReply{Result: m.Result, Err: m.Error}
}
continue
}
if m.Method != "" {
c.mu.Lock()
waiters := c.subs[m.Method]
c.mu.Unlock()
for _, w := range waiters {
select {
case w.ch <- cdpEvent{Method: m.Method, Params: m.Params}:
default: // 满则丢弃,避免阻塞 readLoop
}
}
}
}
}
func (c *Conn) failAll(err error) {
c.mu.Lock()
defer c.mu.Unlock()
c.closed = true
for id, ch := range c.pending {
ch <- cdpReply{Err: &cdpError{Message: "connection lost: " + err.Error()}}
delete(c.pending, id)
}
}
// Close 关闭连接(幂等)。
func (c *Conn) Close() {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return
}
c.closed = true
pending := c.pending
subs := c.subs
c.pending = map[int64]chan cdpReply{}
c.subs = map[string][]*EventWaiter{}
c.mu.Unlock()
for id, ch := range pending {
ch <- cdpReply{Err: &cdpError{Message: "connection closed"}}
delete(pending, id)
}
for _, ws := range subs {
for _, w := range ws {
close(w.ch)
}
}
_ = c.ws.Close(websocket.StatusNormalClosure, "")
}