onesvm-browser-server/server/internal/dock/cdp_client.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

202 lines
5.6 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.

// cdp_client.go:CDP WebSocket 客户端(JSON-RPC id 递增 + pending map)。
//
// 复用声明:调用面与超时语义移植自 bench/site-matrix/cdp_fetch.mjs 的 Cdp 类
// (json/version → webSocketDebuggerUrl → dial → send/recv;方法序列
// Target.createTarget/attachToTarget(flatten) → Page/Runtime/Network.enable →
// Network.setUserAgentOverride → Page.navigate → 等 loadEventFired →
// Runtime.evaluate 提取;detectVendor 正则同源)。
package dock
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
// cdpHTTPTimeout json/version 探测超时(cdp_fetch.mjs:AbortSignal 5s)。
const cdpHTTPTimeout = 5 * time.Second
// cdpClient 单连接 CDP 会话(一次任务一连接,与 cdp_fetch.mjs 同模式)。
type cdpClient struct {
ws *wsConn
nextID int64
idMu sync.Mutex
pending sync.Map // int64 → chan cdpResp
once sync.Once
// eventCh 事件流(Page.loadEventFired / Network.responseReceived 等消费方)。
eventCh chan cdpEvent
}
// cdpResp JSON-RPC 响应。
type cdpResp struct {
ID int64 `json:"id"`
Result json.RawMessage `json:"result"`
Error *cdpErrResp `json:"error"`
}
// cdpEvent JSON-RPC 事件。
type cdpEvent struct {
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
// cdpVersionGet GET {host}/json/version → webSocketDebuggerUrl。
// host 形如 "127.0.0.1:9222"。
func cdpVersionGet(host string) (string, error) {
hc := &http.Client{Timeout: cdpHTTPTimeout}
resp, err := hc.Get("http://" + host + "/json/version")
if err != nil {
return "", fmt.Errorf("dock: GET /json/version: %w", err)
}
defer resp.Body.Close()
var v struct {
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
}
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return "", fmt.Errorf("dock: /json/version 解码: %w", err)
}
if v.WebSocketDebuggerURL == "" {
return "", fmt.Errorf("dock: /json/version 缺 webSocketDebuggerUrl")
}
// rewriteWs(cdp_fetch.mjs 同语义):把 ws host:port 对齐 HTTP 端点
//(调试端点可能回环地址不同)。
return rewriteWsURL(v.WebSocketDebuggerURL, host), nil
}
// rewriteWsURL 把 ws URL 的 host:port 替换为 HTTP 端点的 host:port。
func rewriteWsURL(wsURL, httpHost string) string {
i := strings.Index(wsURL, "://")
if i < 0 {
return wsURL
}
rest := wsURL[i+3:]
if j := strings.Index(rest, "/"); j >= 0 {
return "ws://" + httpHost + rest[j:]
}
return "ws://" + httpHost
}
// newCdpClient 拨号并启动读泵。
func newCdpClient(host string, dialTimeout time.Duration) (*cdpClient, error) {
wsURL, err := cdpVersionGet(host)
if err != nil {
return nil, err
}
ws, err := wsDial(wsURL, dialTimeout)
if err != nil {
return nil, err
}
c := &cdpClient{ws: ws, eventCh: make(chan cdpEvent, 256)}
go c.readLoop()
return c, nil
}
// readLoop 读泵:分发响应到 pending / 事件到 eventCh(cdp_fetch.mjs message 监听同语义)。
func (c *cdpClient) readLoop() {
defer c.closeOnce()
for {
raw, err := c.ws.readFrame()
if err != nil {
return
}
var env struct {
ID *int64 `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
Error *cdpErrResp `json:"error"`
Result json.RawMessage `json:"result"`
}
if json.Unmarshal(raw, &env) != nil {
continue
}
if env.ID != nil {
if chp, ok := c.pending.Load(*env.ID); ok {
c.pending.Delete(*env.ID)
ch := *chp.(*chan cdpResp)
ch <- cdpResp{ID: *env.ID, Result: env.Result, Error: errOf(env.Error)}
}
continue
}
if env.Method != "" {
select {
case c.eventCh <- cdpEvent{Method: env.Method, Params: env.Params}:
default: // 事件缓冲满丢弃(读泵不能阻塞)
}
}
}
}
// Send 发送命令等响应(cdp_fetch.mjs Cdp.send 同语义:id 递增 + 超时)。
func (c *cdpClient) Send(ctx context.Context, method string, params any, sessionID string, timeout time.Duration) (json.RawMessage, error) {
c.idMu.Lock()
id := c.nextID
c.nextID++
c.idMu.Unlock()
req := struct {
ID int64 `json:"id"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
SessionID string `json:"sessionId,omitempty"`
}{ID: id, Method: method, Params: params, SessionID: sessionID}
raw, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("dock: cdp 请求序列化: %w", err)
}
ch := make(chan cdpResp, 2)
c.pending.Store(id, &ch)
defer c.pending.Delete(id)
if err := c.ws.writeFrame(0x1, raw); err != nil {
return nil, fmt.Errorf("dock: cdp 发送 %s: %w", method, err)
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case resp := <-ch:
if resp.Error != nil {
return nil, fmt.Errorf("dock: cdp %s: %s", method, resp.errMsg())
}
return resp.Result, nil
case <-timer.C:
return nil, fmt.Errorf("dock: cdp timeout %s", method)
case <-ctx.Done():
return nil, fmt.Errorf("dock: cdp %s: %w", method, ctx.Err())
}
}
// cdpErrResp JSON-RPC error 对象形状。
type cdpErrResp struct {
Code int `json:"code"`
Message string `json:"message"`
}
// errOf 把 wire 错误转为类型化指针。
func errOf(e *cdpErrResp) *cdpErrResp {
if e == nil {
return nil
}
cp := *e
return &cp
}
// errMsg 错误消息。
func (r cdpResp) errMsg() string {
if r.Error == nil {
return ""
}
return r.Error.Message
}
// Events 事件通道。
func (c *cdpClient) Events() <-chan cdpEvent { return c.eventCh }
// closeOnce 幂等关连接。
func (c *cdpClient) closeOnce() {
c.once.Do(func() { c.ws.Close() })
}
// Close 关闭连接。
func (c *cdpClient) Close() { c.closeOnce() }