From df8c6c48f866f3a442b5c97ac4fbec2be825eaae Mon Sep 17 00:00:00 2001 From: fofolee Date: Thu, 23 Jan 2025 17:44:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E5=8A=9F=E8=83=BD=EF=BC=9A=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=AB=AF=E5=8F=A3=E7=AE=A1=E7=90=86=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=8E=B7=E5=8F=96=E5=92=8C=E5=88=87=E6=8D=A2=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E5=AE=9E=E4=BE=8B=E7=9A=84=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=90=AF=E5=8A=A8=E9=80=89=E9=A1=B9?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E7=A6=81=E7=94=A8=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E5=92=8C=E8=8E=B7=E5=8F=96=E5=BD=93=E5=89=8D=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/quickcomposer/browser/browser.js | 189 ++++++++++++++------ src/js/composer/commands/browserCommands.js | 177 +++++++++++------- 2 files changed, 254 insertions(+), 112 deletions(-) diff --git a/plugin/lib/quickcomposer/browser/browser.js b/plugin/lib/quickcomposer/browser/browser.js index 73a71e8..8dc54d1 100644 --- a/plugin/lib/quickcomposer/browser/browser.js +++ b/plugin/lib/quickcomposer/browser/browser.js @@ -2,11 +2,10 @@ const CDP = require("chrome-remote-interface"); const { exec } = require("child_process"); const path = require("path"); const os = require("os"); -const http = require("http"); -const axios = require("axios"); const fs = require("fs"); +const net = require("net"); -let clients = new Map(); // 存储每个标签页的CDP客户端 +let currentClientPort = null; const getBrowserPath = (browser = "msedge") => { const platform = os.platform(); @@ -70,11 +69,21 @@ const getBrowserPath = (browser = "msedge") => { const isPortAvailable = (port) => { return new Promise((resolve) => { - const server = http.createServer(); - server.listen(port, () => { - server.close(() => resolve(true)); + const socket = new net.Socket(); + + const onError = () => { + socket.destroy(); + resolve(true); + }; + + socket.setTimeout(100); + socket.once("error", onError); + socket.once("timeout", onError); + + socket.connect(port, "127.0.0.1", () => { + socket.destroy(); + resolve(false); }); - server.on("error", () => resolve(false)); }); }; @@ -82,8 +91,8 @@ const waitForPort = async (port, timeout = 30000) => { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { - const response = await axios.get(`http://localhost:${port}/json/version`); - if (response.status === 200) return true; + await CDP.Version({ port }); + return true; } catch (e) { await new Promise((resolve) => setTimeout(resolve, 100)); } @@ -112,6 +121,7 @@ const launchBrowser = async (options) => { windowSize = null, incognito = false, headless = false, + disableExtensions = false, } = options; if (!browserPath) { @@ -119,6 +129,7 @@ const launchBrowser = async (options) => { } const port = await findAvailablePort(9222); + currentClientPort = port; const automationArgs = [ `--remote-debugging-port=${port}`, @@ -142,6 +153,7 @@ const launchBrowser = async (options) => { proxy ? `--proxy-server=${proxy}` : "", incognito ? incognitoArg[browserType] : "", headless ? "--headless" : "", + disableExtensions ? "--disable-extensions" : "", useSingleUserDataDir ? `--user-data-dir=${path.join( os.tmpdir(), @@ -198,40 +210,93 @@ const killRunningBrowser = (browserType = "msedge") => { }); }; -const initCDP = async (targetId) => { - if (!clients.has(targetId)) { - try { - const client = await CDP({ target: targetId }); - const { Page, Runtime, Target, Network, Emulation, DOM } = client; - await Promise.all([Page.enable(), Runtime.enable(), DOM.enable()]); - clients.set(targetId, { - client, - Page, - Runtime, - Target, - Network, - Emulation, - DOM, - }); - } catch (err) { - console.log(err); - throw new Error(`请先通过浏览器控制中的"启动浏览器"打开浏览器`); +const getClientPorts = async (returnFirstPort = false) => { + try { + // 创建所有端口检查的 Promise 数组 + const portChecks = []; + for (let port = 9222; port < 9322; port++) { + portChecks.push( + CDP.List({ port }) + .then(() => port) + .catch(() => null) + ); } + + if (returnFirstPort) { + // 如果需要返回第一个可用端口,使用 Promise.race + const firstPort = await Promise.race(portChecks); + if (firstPort) { + return firstPort; + } else { + return null; + } + } + + // 如果不需要返回第一个端口或没有找到可用端口,并行执行所有检查 + const results = await Promise.all(portChecks); + + // 过滤出可用的端口 + return results.filter((port) => port !== null); + } catch (error) { + throw new Error(`获取客户端列表失败: ${error.message}`); + } +}; + +const getCurrentClientPort = async () => { + if (currentClientPort === null) { + const port = await getClientPorts(true); + if (port === null) { + throw new Error("未找到可用的浏览器实例,请先从实例管理里面启动新的实例"); + } + currentClientPort = port; + } + return currentClientPort; +}; + +const getTargets = async () => { + const port = await getCurrentClientPort(); + return await CDP.List({ port }); +}; + +const initCDP = async (targetId) => { + try { + const port = await getCurrentClientPort(); + const client = await CDP({ + target: targetId, + port, + }); + + const { Page, Runtime, Target, Network, Emulation, DOM } = client; + await Promise.all([Page.enable(), Runtime.enable(), DOM.enable()]); + + return { + client, + Page, + Runtime, + Target, + Network, + Emulation, + DOM, + }; + } catch (err) { + console.log(err); + throw new Error(`连接到浏览器失败: ${err.message}`); } - return clients.get(targetId); }; const cleanupCDP = async (targetId) => { - const client = clients.get(targetId); - if (client) { - await client.client.close(); - clients.delete(targetId); + try { + // 直接关闭传入的 client + if (targetId?.client) { + await targetId.client.close(); + } + } catch (error) { + console.log("关闭CDP连接失败:", error); } }; -// 获取所有标签页 const getTabs = async () => { - const targets = await CDP.List(); + const targets = await getTargets(); return targets .filter((target) => target.type === "page") .map((target) => ({ @@ -241,10 +306,8 @@ const getTabs = async () => { })); }; -// 获取当前活动标签页 const getCurrentTab = async () => { - const targets = await CDP.List(); - // 一般排第一个的就是活动标签页 + const targets = await getTargets(); const currentTarget = targets.find((target) => target.type === "page"); if (!currentTarget) { @@ -258,14 +321,13 @@ const getCurrentTab = async () => { }; }; -// 搜索标签页 const searchTarget = async (tab) => { if (!tab || !tab.by || !tab.searchValue || tab.by === "active") { const currentTab = await getCurrentTab(); return currentTab; } - const targets = await CDP.List(); + const targets = await getTargets(); const target = targets.find((target) => target[tab.by].includes(tab.searchValue) ); @@ -275,13 +337,12 @@ const searchTarget = async (tab) => { return target; }; -// 激活指定标签页 const activateTab = async (tab) => { const target = await searchTarget(tab); - await CDP.Activate({ id: target.id }); + const port = await getCurrentClientPort(); + await CDP.Activate({ id: target.id, port }); }; -// 创建新标签页 const createNewTab = async (url = "about:blank") => { const currentTab = await getCurrentTab(); const { Target } = await initCDP(currentTab.id); @@ -295,11 +356,11 @@ const createNewTab = async (url = "about:blank") => { }; }; -// 关闭标签页 const closeTab = async (tab) => { const target = await searchTarget(tab); + const port = await getCurrentClientPort(); await cleanupCDP(target.id); - await CDP.Close({ id: target.id }); + await CDP.Close({ id: target.id, port }); }; const getUrl = async (tab) => { @@ -349,7 +410,6 @@ const setCookie = async (tab, cookies, options = {}) => { const target = await searchTarget(tab); const { Network, Page } = await initCDP(target.id); try { - // 直接从Page获取URL,避免创建新连接 const { frameTree } = await Page.getFrameTree(); const url = frameTree.frame.url; @@ -382,15 +442,10 @@ const getCookie = async (tab, name) => { // 捕获标签页截图 const captureScreenshot = async (tab, options = {}) => { const target = await searchTarget(tab); - const { - format = "png", - quality = 100, - savePath, - selector = null, - } = options; + const { format = "png", quality = 100, savePath, selector = null } = options; try { - const { Page, Emulation, DOM } = await initCDP(target.id); + const { Page, DOM } = await initCDP(target.id); await DOM.enable(); let clip = null; @@ -461,6 +516,31 @@ const captureScreenshot = async (tab, options = {}) => { } }; +const destroyClientByPort = async (port) => { + try { + const client = await CDP({ port }); + await client.Browser.close(); + + if (port === currentClientPort) { + currentClientPort = null; + } + } catch (error) { + throw new Error(`销毁客户端失败,请手动关闭`); + } +}; + +const switchClientByPort = async (port) => { + try { + const versionInfo = await CDP.Version({ port }); + if (!versionInfo) { + throw new Error(`端口 ${port} 未找到活动的浏览器实例`); + } + currentClientPort = port; + } catch (error) { + throw new Error(`切换客户端失败: ${error.message}`); + } +}; + module.exports = { launchBrowser, killRunningBrowser, @@ -475,4 +555,9 @@ module.exports = { setCookie, getCookie, captureScreenshot, + getClientPorts, + destroyClientByPort, + switchClientByPort, + getCurrentClientPort, + getTargets, }; diff --git a/src/js/composer/commands/browserCommands.js b/src/js/composer/commands/browserCommands.js index bcc62a2..458f9c5 100644 --- a/src/js/composer/commands/browserCommands.js +++ b/src/js/composer/commands/browserCommands.js @@ -34,76 +34,133 @@ export const browserCommands = { commands: [ { value: "quickcomposer.browser.launchBrowser", - label: "启动浏览器", + label: "浏览器实例管理", icon: "launch", isAsync: true, - config: [ + config: [], + subCommands: [ { - component: "OptionEditor", - icon: "settings", - width: 12, - options: { - browserType: { - component: "ButtonGroup", - defaultValue: "msedge", - options: [ - { label: "Edge", value: "msedge" }, - { label: "Chrome", value: "chrome" }, - ], - width: 12, - }, - useSingleUserDataDir: { - label: "使用独立用户数据目录", - component: "CheckButton", - width: 4, - }, - headless: { - label: "无头模式", - component: "CheckButton", - width: 4, - }, - incognito: { - label: "隐身模式", - component: "CheckButton", - width: 4, - }, - windowSize: { - label: "窗口尺寸", - component: "VariableInput", - icon: "window", - width: 6, - placeholder: "如1920x1080,不设置则最大化", - }, - proxy: { - label: "代理", - component: "VariableInput", - icon: "vpn_lock", - width: 6, - placeholder: "如 socks5://127.0.0.1:7890", - }, - browserPath: { - label: "浏览器路径", - component: "VariableInput", - icon: "folder", + value: "quickcomposer.browser.launchBrowser", + label: "启动浏览器实例", + icon: "launch", + config: [ + { + component: "OptionEditor", + icon: "settings", width: 12, options: { - dialog: { - type: "open", + browserType: { + component: "ButtonGroup", + defaultValue: "msedge", + options: [ + { label: "Edge", value: "msedge" }, + { label: "Chrome", value: "chrome" }, + ], + width: 12, + }, + useSingleUserDataDir: { + label: "使用独立用户数据目录", + component: "CheckButton", + width: 3, + }, + headless: { + label: "无头模式", + component: "CheckButton", + width: 3, + }, + incognito: { + label: "隐身模式", + component: "CheckButton", + width: 3, + }, + disableExtensions: { + label: "禁用扩展", + component: "CheckButton", + width: 3, + }, + windowSize: { + label: "窗口尺寸", + component: "VariableInput", + icon: "window", + width: 6, + placeholder: "如1920x1080,不设置则最大化", + }, + proxy: { + label: "代理", + component: "VariableInput", + icon: "vpn_lock", + width: 6, + placeholder: "如 socks5://127.0.0.1:7890", + }, + browserPath: { + label: "浏览器路径", + component: "VariableInput", + icon: "folder", + width: 12, options: { - title: "选择浏览器", - properties: ["openFile"], + dialog: { + type: "open", + options: { + title: "选择浏览器", + properties: ["openFile"], + }, + }, }, + placeholder: "二进制绝对路径,留空则自动查找", }, }, - placeholder: "二进制绝对路径,留空则自动查找", + defaultValue: { + browserType: "msedge", + useSingleUserDataDir: true, + headless: false, + incognito: false, + }, }, - }, - defaultValue: { - browserType: "msedge", - useSingleUserDataDir: true, - headless: false, - incognito: false, - }, + ], + }, + { + value: "quickcomposer.browser.getClientPorts", + label: "获取所有浏览器实例端口", + icon: "list", + }, + { + value: "quickcomposer.browser.destroyClientByPort", + label: "关闭浏览器实例", + icon: "close", + config: [ + { + label: "浏览器实例端口", + component: "NumberInput", + icon: "label", + width: 12, + defaultValue: 9222, + min: 9222, + max: 9322, + step: 1, + }, + ], + }, + { + value: "quickcomposer.browser.getCurrentClientPort", + label: "获取当前操控的实例端口", + icon: "label", + }, + { + value: "quickcomposer.browser.switchClientByPort", + label: "切换要操控的实例", + icon: "switch_account", + config: [ + { + label: "浏览器实例端口", + component: "NumberInput", + icon: "label", + width: 12, + defaultValue: 9222, + min: 9222, + max: 9322, + step: 1, + }, + ], }, ], },