Cette page est affichée en anglais. Une traduction française est en cours.
ScriptsMar 24, 2026·1 min de lecture

CDP WebSocket

Chrome DevTools Protocol WebSocket

Prêt pour agents

Staging sûr pour cet actif

Cet actif est d'abord staged. Le prompt copié demande à l'agent d'inspecter les fichiers staged avant d'activer scripts, config MCP ou config globale.

Stage only · 19/100Policy : staging
Surface agent
Tout agent MCP/CLI
Type
Script
Installation
Stage only
Confiance
Éditeur vérifié
Point d'entrée
CDP WebSocket 代理(多 Agent 共享浏览器)
Commande de staging sûr
npx -y tokrepo@latest install e0f83e28-a379-48bb-af1b-ff685c17285f --target codex

Stage les fichiers d'abord; l'activation exige la revue du README et du plan staged.

#!/usr/bin/env node /**

  • CDP Background Proxy v3 — 持久连接 + 多 Agent 隔离
    1. 持久 WebSocket 连接到 Chrome(弹窗只出现一次)
    1. 拦截 Target.activateTarget / Page.bringToFront(防抢焦点)
    1. 强制 createTarget background=true
    1. 请求 ID 重映射(防多客户端 ID 冲突)
    1. 事件按 sessionId 路由到对应客户端(防多 Agent 互扰)
    1. 自动重连
  • 依赖: npm install ws --prefix "$HOME/scripts" (ws 包需与本文件同目录层级)
  • 用法: node cdp-proxy.mjs [--port 9401] [--chrome-port 9222] */

import http from 'http'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { WebSocketServer, WebSocket } from 'ws';

const args = process.argv.slice(2); const getArg = (name, def) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : def; };

const PROXY_PORT = parseInt(getArg('--port', '9401')); const CHROME_PORT = parseInt(getArg('--chrome-port', '9222')); const CHROME_HOST = getArg('--chrome-host', '127.0.0.1');

const DEVTOOLS_PORT_FILE = getArg('--devtools-port-file', path.join(os.homedir(), 'Library/Application Support/Google/Chrome/DevToolsActivePort'));

// ═══════════════════════════════════════════ // 持久 Chrome 连接 // ═══════════════════════════════════════════

let chromeWs = null; let chromeReady = false; let reconnecting = false;

const BLOCKED_METHODS = new Set([ 'Target.activateTarget', 'Page.bringToFront', ]);

function safeSend(ws, data) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(typeof data === 'string' ? data : JSON.stringify(data)); } }

function readDevToolsActivePort() { try { const content = fs.readFileSync(DEVTOOLS_PORT_FILE, 'utf-8').trim(); const lines = content.split('\n'); if (lines.length >= 2) { return { port: parseInt(lines[0]), wsPath: lines[1] }; } } catch (e) { /* ignore */ } return null; }

function getChromeWsUrl() { const portInfo = readDevToolsActivePort(); if (portInfo) { return ws://${CHROME_HOST}:${portInfo.port}${portInfo.wsPath}; } return null; }

// ═══════════════════════════════════════════ // 多客户端隔离 // ═══════════════════════════════════════════

// 全局自增 ID,保证唯一 let globalIdCounter = 1;

// proxyId → { clientWs, originalId, method } const pendingRequests = new Map();

// sessionId → clientWs(哪个客户端 attach 了这个 session) const sessionOwners = new Map();

// clientWs → { tabs: Set, sessions: Set, proxyIds: Set } const clientState = new Map();

function getOrCreateState(clientWs) { if (!clientState.has(clientWs)) { clientState.set(clientWs, { tabs: new Set(), sessions: new Set(), proxyIds: new Set() }); } return clientState.get(clientWs); }

// ═════════════════════���═════════════════════ // Chrome 连接管理 // ═══════════════════════════════════════════

function connectToChrome() { if (reconnecting) return; reconnecting = true;

const wsUrl = getChromeWsUrl();
if (!wsUrl) {
    console.error('[Proxy] Chrome 未运行,5秒后重试...');
    setTimeout(() => { reconnecting = false; connectToChrome(); }, 5000);
    return;
}

console.log(`[Proxy] 连接 Chrome: ${wsUrl}`);
chromeWs = new WebSocket(wsUrl);

chromeWs.on('open', () => {
    console.log('[Proxy] ✓ Chrome 已连接');
    chromeReady = true;
    reconnecting = false;
});

chromeWs.on('message', (rawData) => {
    try {
        const msg = JSON.parse(rawData.toString());

        // ── 响应消息(有 id)→ 路由到发起者 ──
        if (msg.id !== undefined && pendingRequests.has(msg.id)) {
            const { clientWs, originalId, method } = pendingRequests.get(msg.id);
            pendingRequests.delete(msg.id);

            const state = clientState.get(clientWs);

            // 追踪 createTarget
            if (method === 'Target.createTarget' && msg.result?.targetId) {
                if (state) state.tabs.add(msg.result.targetId);
                console.log(`[Proxy] 新 tab: ${msg.result.targetId}`);
            }

            // 追踪 attachToTarget → 记录 session 归属
            if (method === 'Target.attachToTarget' && msg.result?.sessionId) {
                const sid = msg.result.sessionId;
                sessionOwners.set(sid, clientWs);
                if (state) state.sessions.add(sid);
                console.log(`[Proxy] session ${sid.slice(0, 8)}... → 客户端`);
            }

            // 还原原始 ID
            msg.id = originalId;
            safeSend(clientWs, msg);
            return;
        }

        // ── 事件消息(无 id)→ 按 sessionId 路由 ──
        if (msg.method) {
            // 带 sessionId 的事件:发给对应客户端
            if (msg.sessionId && sessionOwners.has(msg.sessionId)) {
                safeSend(sessionOwners.get(msg.sessionId), msg);
                return;
            }

            // Target 域的全局事件:按 targetId 路由或广播
            if (msg.method.startsWith('Target.')) {
                const targetId = msg.params?.targetInfo?.targetId || msg.params?.targetId;
                if (targetId) {
                    // 找到拥有这个 tab 的客户端
                    for (const [ws, state] of clientState) {
                        if (state.tabs.has(targetId)) {
                            safeSend(ws, msg);
                            return;
                        }
                    }
                }
                // 找不到归属,广播
                for (const [ws] of clientState) {
                    safeSend(ws, msg);
                }
                return;
            }

            // 其他无 sessionId 的事件,广播
            for (const [ws] of clientState) {
                safeSend(ws, msg);
            }
            return;
        }

        // 其他消息,广播
        for (const [ws] of clientState) {
            safeSend(ws, rawData);
        }
    } catch (e) {
        for (const [ws] of clientState) {
            safeSend(ws, rawData);
        }
    }
});

chromeWs.on('close', () => {
    console.log('[Proxy] Chrome 断开,5秒后重连...');
    chromeReady = false;
    chromeWs = null;
    reconnecting = false;
    // 清理所有 session 归属(Chrome 重启后 session 失效)
    sessionOwners.clear();
    setTimeout(() => connectToChrome(), 5000);
});

chromeWs.on('error', (err) => {
    console.error('[Proxy] Chrome 错误:', err.message);
    chromeReady = false;
    chromeWs = null;
    reconnecting = false;
    setTimeout(() => connectToChrome(), 5000);
});

}

// ═══════════════════════════════════════════ // HTTP 端点 // ═══════════════════════════════════════════

function cdpRequest(method, params = {}) { return new Promise((resolve, reject) => { if (!chromeReady) return reject(new Error('Chrome not connected')); const proxyId = globalIdCounter++; const timeout = setTimeout(() => { pendingRequests.delete(proxyId); reject(new Error('timeout')); }, 5000); const fakeClient = { send: (data) => { clearTimeout(timeout); resolve(typeof data === 'string' ? JSON.parse(data) : data); }, readyState: WebSocket.OPEN, }; pendingRequests.set(proxyId, { clientWs: fakeClient, originalId: proxyId, method }); chromeWs.send(JSON.stringify({ id: proxyId, method, params })); }); }

const server = http.createServer(async (req, res) => { try { if (req.url === '/json/version') { const portInfo = readDevToolsActivePort(); const wsUrl = portInfo ? ws://${CHROME_HOST}:${PROXY_PORT}${portInfo.wsPath} : ws://${CHROME_HOST}:${PROXY_PORT}/devtools/browser/proxy; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ Browser: 'Chrome (via CDP Proxy v3)', webSocketDebuggerUrl: wsUrl })); return; }

    if (req.url === '/json/list' || req.url === '/json') {
        if (!chromeReady) { res.writeHead(503); res.end('Chrome not connected'); return; }
        try {
            const result = await cdpRequest('Target.getTargets');
            const targets = (result.result?.targetInfos || []).map(t => ({
                ...t,
                webSocketDebuggerUrl: `ws://${CHROME_HOST}:${PROXY_PORT}/devtools/page/${t.targetId}`,
            }));
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify(targets));
        } catch (e) { res.writeHead(500); res.end(e.message); }
        return;
    }

    if (req.url === '/json/new') {
        if (!chromeReady) { res.writeHead(503); res.end('Chrome not connected'); return; }
        try {
            const result = await cdpRequest('Target.createTarget', { url: 'about:blank', background: true });
            res.writeHead(200, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify(result.result || {}));
        } catch (e) { res.writeHead(500); res.end(e.message); }
        return;
    }

    if (req.url === '/proxy/status') {
        const clientList = [];
        for (const [, state] of clientState) {
            clientList.push({ tabs: state.tabs.size, sessions: state.sessions.size });
        }
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            chromeConnected: chromeReady,
            clients: clientState.size,
            sessions: sessionOwners.size,
            pendingRequests: pendingRequests.size,
            clientDetails: clientList,
        }, null, 2));
        return;
    }

    res.writeHead(404);
    res.end('Not found');
} catch (e) { res.writeHead(500); res.end(e.message); }

});

// ═══════════════════���═══════════════════════ // WebSocket 客户端处理 // ═══════════════════════════════════════════

const wss = new WebSocketServer({ server });

wss.on('connection', (clientWs, req) => { const state = getOrCreateState(clientWs); console.log([Proxy] 客户端连接 (共 ${clientState.size} 个));

clientWs.on('message', (data) => {
    if (!chromeReady) {
        try {
            const msg = JSON.parse(data.toString());
            if (msg.id !== undefined) {
                safeSend(clientWs, { id: msg.id, error: { code: -1, message: 'Chrome not connected' } });
            }
        } catch (e) { /* ignore */ }
        return;
    }

    try {
        const msg = JSON.parse(data.toString());

        // 拦截抢焦点命令
        if (BLOCKED_METHODS.has(msg.method)) {
            console.log(`[Proxy] 拦截: ${msg.method}`);
            const reply = { id: msg.id, result: {} };
            if (msg.sessionId) reply.sessionId = msg.sessionId;
            safeSend(clientWs, reply);
            return;
        }

        // 强制后台创建 tab
        if (msg.method === 'Target.createTarget') {
            if (!msg.params) msg.params = {};
            msg.params.background = true;
            console.log(`[Proxy] 后台创建 tab: ${msg.params.url || 'about:blank'}`);
        }

        // ── ID 重映射 ──
        if (msg.id !== undefined) {
            const proxyId = globalIdCounter++;
            pendingRequests.set(proxyId, {
                clientWs,
                originalId: msg.id,
                method: msg.method,
            });
            state.proxyIds.add(proxyId);
            msg.id = proxyId;
        }

        safeSend(chromeWs, msg);
    } catch (e) {
        safeSend(chromeWs, data);
    }
});

const cleanup = () => {
    // 清理这个客户端的 session 归属
    for (const sid of state.sessions) {
        sessionOwners.delete(sid);
    }
    // 清理未完成的请求
    for (const pid of state.proxyIds) {
        pendingRequests.delete(pid);
    }
    clientState.delete(clientWs);
    console.log(`[Proxy] 客户端断开 (剩 ${clientState.size} 个, 释放 ${state.tabs.size} tab, ${state.sessions.size} session)`);
};

clientWs.on('close', cleanup);
clientWs.on('error', cleanup);

});

// ═══════════════════════════════════════════ // 启动 // ═══════════════════════════════════════════

process.on('uncaughtException', (err) => { console.error('[Proxy] 异常(已恢复):', err.message); }); process.on('unhandledRejection', (reason) => { console.error('[Proxy] Promise 拒绝(已恢复):', reason); });

server.listen(PROXY_PORT, () => { console.log(╔══════════════════════════════════════════════════════╗ ║ CDP Background Proxy v3 — 持久连接 + 多Agent隔离 ║ ║ ║ ║ Proxy: http://127.0.0.1:${PROXY_PORT} ║ ║ ║ ║ ✓ 持久连接(弹窗只出现一次) ║ ║ ✓ 拦截 activateTarget / bringToFront ║ ║ ✓ 强制 createTarget background=true ║ ║ ✓ 请求 ID 重映射(防多客户端冲突) ║ ║ ✓ 事件按 sessionId 路由(防多 Agent 互扰) ║ ║ ✓ ���动重连 ║ ║ ║ ║ 状态: http://127.0.0.1:${PROXY_PORT}/proxy/status ║ ╚══════════════════════════════════════════════════════╝); connectToChrome(); });

Fil de discussion

Connectez-vous pour rejoindre la discussion.
Aucun commentaire pour l'instant. Soyez le premier à partager votre avis.

Actifs similaires