From 179c0c567ff2de7b620c2520beea3cd1fa3a0857 Mon Sep 17 00:00:00 2001 From: fofolee Date: Thu, 23 Jan 2025 18:43:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/quickcomposer/browser/browser.js | 563 ------------------ plugin/lib/quickcomposer/browser/cdp.js | 44 ++ plugin/lib/quickcomposer/browser/client.js | 286 +++++++++ plugin/lib/quickcomposer/browser/cookie.js | 40 ++ .../lib/quickcomposer/browser/execScript.js | 31 +- .../lib/quickcomposer/browser/getSelector.js | 2 +- plugin/lib/quickcomposer/browser/index.js | 12 +- .../lib/quickcomposer/browser/screenshot.js | 84 +++ plugin/lib/quickcomposer/browser/tabs.js | 86 +++ plugin/lib/quickcomposer/browser/url.js | 23 + src/js/composer/commands/browserCommands.js | 16 +- 11 files changed, 612 insertions(+), 575 deletions(-) delete mode 100644 plugin/lib/quickcomposer/browser/browser.js create mode 100644 plugin/lib/quickcomposer/browser/cdp.js create mode 100644 plugin/lib/quickcomposer/browser/client.js create mode 100644 plugin/lib/quickcomposer/browser/cookie.js create mode 100644 plugin/lib/quickcomposer/browser/screenshot.js create mode 100644 plugin/lib/quickcomposer/browser/tabs.js create mode 100644 plugin/lib/quickcomposer/browser/url.js diff --git a/plugin/lib/quickcomposer/browser/browser.js b/plugin/lib/quickcomposer/browser/browser.js deleted file mode 100644 index 8dc54d1..0000000 --- a/plugin/lib/quickcomposer/browser/browser.js +++ /dev/null @@ -1,563 +0,0 @@ -const CDP = require("chrome-remote-interface"); -const { exec } = require("child_process"); -const path = require("path"); -const os = require("os"); -const fs = require("fs"); -const net = require("net"); - -let currentClientPort = null; - -const getBrowserPath = (browser = "msedge") => { - const platform = os.platform(); - let paths = null; - if (platform === "win32") { - paths = { - chrome: [ - path.join( - process.env["ProgramFiles"], - "Google/Chrome/Application/chrome.exe" - ), - path.join( - process.env["ProgramFiles(x86)"], - "Google/Chrome/Application/chrome.exe" - ), - path.join( - process.env["LocalAppData"], - "Google/Chrome/Application/chrome.exe" - ), - ], - msedge: [ - path.join( - process.env["ProgramFiles"], - "Microsoft/Edge/Application/msedge.exe" - ), - path.join( - process.env["ProgramFiles(x86)"], - "Microsoft/Edge/Application/msedge.exe" - ), - path.join( - process.env["LocalAppData"], - "Microsoft/Edge/Application/msedge.exe" - ), - ], - }; - } else if (platform === "darwin") { - paths = { - chrome: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"], - msedge: [ - "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", - ], - }; - } else if (platform === "linux") { - paths = { - chrome: [ - "/opt/google/chrome/chrome", - "/usr/bin/google-chrome", - "/usr/bin/google-chrome-stable", - ], - msedge: [ - "/opt/microsoft/msedge/msedge", - "/usr/bin/microsoft-edge", - "/usr/bin/microsoft-edge-stable", - ], - }; - } else { - throw new Error("不支持的操作系统"); - } - return paths[browser].find((p) => fs.existsSync(p)); -}; - -const isPortAvailable = (port) => { - return new Promise((resolve) => { - 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); - }); - }); -}; - -const waitForPort = async (port, timeout = 30000) => { - const startTime = Date.now(); - while (Date.now() - startTime < timeout) { - try { - await CDP.Version({ port }); - return true; - } catch (e) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } - return false; -}; - -const findAvailablePort = async (startPort) => { - let port = startPort; - while (port < startPort + 100) { - const available = await isPortAvailable(port); - if (available) { - return port; - } - port++; - } - throw new Error("无法找到可用的调试端口"); -}; - -const launchBrowser = async (options) => { - const { - browserType = "msedge", - useSingleUserDataDir = true, - proxy = null, - browserPath = getBrowserPath(browserType), - windowSize = null, - incognito = false, - headless = false, - disableExtensions = false, - } = options; - - if (!browserPath) { - throw new Error("未找到浏览器,或未指定浏览器路径"); - } - - const port = await findAvailablePort(9222); - currentClientPort = port; - - const automationArgs = [ - `--remote-debugging-port=${port}`, - "--disable-infobars", - "--disable-notifications", - "--disable-popup-blocking", - "--disable-save-password-bubble", - "--disable-translate", - "--no-first-run", - "--no-default-browser-check", - "--user-data-start-with-quickcomposer", - ]; - - const incognitoArg = { - chrome: "--incognito", - msedge: "--inprivate", - }; - - const optionArgs = [ - windowSize ? `--window-size=${windowSize}` : "--start-maximized", - proxy ? `--proxy-server=${proxy}` : "", - incognito ? incognitoArg[browserType] : "", - headless ? "--headless" : "", - disableExtensions ? "--disable-extensions" : "", - useSingleUserDataDir - ? `--user-data-dir=${path.join( - os.tmpdir(), - `${browserType}-debug-${port}` - )}` - : "", - ].filter(Boolean); - - const args = [...automationArgs, ...optionArgs]; - - return new Promise(async (resolve, reject) => { - if (!useSingleUserDataDir) { - try { - await killRunningBrowser(browserType); - } catch (e) { - reject(e); - return; - } - } - const child = exec( - `"${browserPath}" ${args.join(" ")}`, - { windowsHide: true }, - async (error) => { - if (error) { - reject(error); - return; - } - } - ); - - waitForPort(port).then((success) => { - if (success) { - resolve({ pid: child.pid, port }); - } else { - reject(new Error("浏览器启动超时,请检查是否有权限问题或防火墙限制")); - } - }); - }); -}; - -const killRunningBrowser = (browserType = "msedge") => { - return new Promise((resolve, reject) => { - if (os.platform() === "win32") { - exec(`taskkill /F /IM ${browserType}.exe`, (error) => { - if (error) reject(error); - else resolve(); - }); - } else { - exec(`kill -9 $(pgrep ${browserType})`, (error) => { - if (error) reject(error); - else resolve(); - }); - } - }); -}; - -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}`); - } -}; - -const cleanupCDP = async (targetId) => { - try { - // 直接关闭传入的 client - if (targetId?.client) { - await targetId.client.close(); - } - } catch (error) { - console.log("关闭CDP连接失败:", error); - } -}; - -const getTabs = async () => { - const targets = await getTargets(); - return targets - .filter((target) => target.type === "page") - .map((target) => ({ - url: target.url, - title: target.title, - id: target.id, - })); -}; - -const getCurrentTab = async () => { - const targets = await getTargets(); - const currentTarget = targets.find((target) => target.type === "page"); - - if (!currentTarget) { - throw new Error("未找到当前活动标签页"); - } - - return { - url: currentTarget.url, - title: currentTarget.title, - id: currentTarget.id, - }; -}; - -const searchTarget = async (tab) => { - if (!tab || !tab.by || !tab.searchValue || tab.by === "active") { - const currentTab = await getCurrentTab(); - return currentTab; - } - - const targets = await getTargets(); - const target = targets.find((target) => - target[tab.by].includes(tab.searchValue) - ); - if (!target) { - throw new Error(`未找到目标标签页: ${tab.by} = ${tab.searchValue}`); - } - return target; -}; - -const activateTab = async (tab) => { - const target = await searchTarget(tab); - 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); - const { targetId } = await Target.createTarget({ url }); - const { targetInfo } = await Target.getTargetInfo({ targetId }); - await cleanupCDP(currentTab.id); - return { - url: targetInfo.url, - title: targetInfo.title, - id: targetId, - }; -}; - -const closeTab = async (tab) => { - const target = await searchTarget(tab); - const port = await getCurrentClientPort(); - await cleanupCDP(target.id); - await CDP.Close({ id: target.id, port }); -}; - -const getUrl = async (tab) => { - const target = await searchTarget(tab); - const { Page } = await initCDP(target.id); - const { frameTree } = await Page.getFrameTree(); - await cleanupCDP(target.id); - return frameTree.frame.url; -}; - -const setUrl = async (tab, url) => { - const target = await searchTarget(tab); - const { Page } = await initCDP(target.id); - await Page.navigate({ url }); - await Page.loadEventFired(); - await cleanupCDP(target.id); -}; - -const executeScript = async (tab, script, args = {}) => { - const target = await searchTarget(tab); - try { - const { Runtime } = await initCDP(target.id); - const argNames = Object.keys(args); - const argValues = Object.values(args).map((v) => JSON.stringify(v)); - - const wrappedScript = ` - (async function(${argNames.join(", ")}) { - ${script} - })(${argValues.join(", ")}) - `; - - const { result } = await Runtime.evaluate({ - expression: wrappedScript, - returnByValue: true, - awaitPromise: true, - }); - - await cleanupCDP(target.id); - return result.value; - } catch (e) { - console.log(e); - throw new Error("执行脚本失败"); - } -}; - -const setCookie = async (tab, cookies, options = {}) => { - const target = await searchTarget(tab); - const { Network, Page } = await initCDP(target.id); - try { - const { frameTree } = await Page.getFrameTree(); - const url = frameTree.frame.url; - - for (const cookie of cookies) { - await Network.setCookie({ - name: cookie.name, - value: cookie.value, - domain: options.domain || url.split("/")[2], - path: options.path || "/", - secure: options.secure || false, - expires: options.expires - ? Math.floor(Date.now() / 1000) + options.expires * 3600 - : undefined, - }); - } - } finally { - await cleanupCDP(target.id); - } -}; - -const getCookie = async (tab, name) => { - const target = await searchTarget(tab); - const { Network } = await initCDP(target.id); - const { cookies } = await Network.getCookies(); - await cleanupCDP(target.id); - if (!name) return cookies; - return cookies.find((cookie) => cookie.name === name); -}; - -// 捕获标签页截图 -const captureScreenshot = async (tab, options = {}) => { - const target = await searchTarget(tab); - const { format = "png", quality = 100, savePath, selector = null } = options; - - try { - const { Page, DOM } = await initCDP(target.id); - await DOM.enable(); - - let clip = null; - if (selector) { - // 获取DOM节点 - const { root } = await DOM.getDocument(); - const { nodeId } = await DOM.querySelector({ - nodeId: root.nodeId, - selector: selector, - }); - - if (!nodeId) { - throw new Error(`未找到元素: ${selector}`); - } - - // 获取元素的精确四边形坐标 - const { quads } = await DOM.getContentQuads({ nodeId }); - if (!quads || quads.length === 0) { - throw new Error("无法获取元素位置信息"); - } - - // 获取布局指标 - const { visualViewport } = await Page.getLayoutMetrics(); - const { pageX, pageY } = visualViewport; - - // 计算边界框 - const quad = quads[0]; - const x = Math.min(quad[0], quad[2], quad[4], quad[6]); - const y = Math.min(quad[1], quad[3], quad[5], quad[7]); - const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; - const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; - - clip = { - x: Math.round(x - pageX), - y: Math.round(y - pageY), - width: Math.round(width), - height: Math.round(height), - scale: 1, - }; - - // 确保尺寸不为0 - if (clip.width === 0) clip.width = 1; - if (clip.height === 0) clip.height = 1; - } - - const screenshotParams = { - format, - quality: format === "jpeg" ? quality : undefined, - fromSurface: true, - captureBeyondViewport: !!selector, - }; - - if (clip) { - screenshotParams.clip = clip; - } - - const { data } = await Page.captureScreenshot(screenshotParams); - - await DOM.disable(); - - if (savePath) { - fs.writeFileSync(savePath, data, "base64"); - } - - return data; - } finally { - await cleanupCDP(target.id); - } -}; - -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, - getTabs, - getCurrentTab, - activateTab, - createNewTab, - closeTab, - getUrl, - setUrl, - executeScript, - setCookie, - getCookie, - captureScreenshot, - getClientPorts, - destroyClientByPort, - switchClientByPort, - getCurrentClientPort, - getTargets, -}; diff --git a/plugin/lib/quickcomposer/browser/cdp.js b/plugin/lib/quickcomposer/browser/cdp.js new file mode 100644 index 0000000..9565da9 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/cdp.js @@ -0,0 +1,44 @@ +const CDP = require("chrome-remote-interface"); +const { getCurrentClientPort } = require("./client"); + +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}`); + } +}; + +const cleanupCDP = async (targetId) => { + try { + // 直接关闭传入的 client + if (targetId?.client) { + await targetId.client.close(); + } + } catch (error) { + console.log("关闭CDP连接失败:", error); + } +}; + +module.exports = { + initCDP, + cleanupCDP, +}; diff --git a/plugin/lib/quickcomposer/browser/client.js b/plugin/lib/quickcomposer/browser/client.js new file mode 100644 index 0000000..38a402c --- /dev/null +++ b/plugin/lib/quickcomposer/browser/client.js @@ -0,0 +1,286 @@ +const { exec } = require("child_process"); +const path = require("path"); +const os = require("os"); +const fs = require("fs"); +const net = require("net"); +const CDP = require("chrome-remote-interface"); + +let currentClientPort = null; + +const getBrowserPath = (browser = "msedge") => { + const platform = os.platform(); + let paths = null; + if (platform === "win32") { + paths = { + chrome: [ + path.join( + process.env["ProgramFiles"], + "Google/Chrome/Application/chrome.exe" + ), + path.join( + process.env["ProgramFiles(x86)"], + "Google/Chrome/Application/chrome.exe" + ), + path.join( + process.env["LocalAppData"], + "Google/Chrome/Application/chrome.exe" + ), + ], + msedge: [ + path.join( + process.env["ProgramFiles"], + "Microsoft/Edge/Application/msedge.exe" + ), + path.join( + process.env["ProgramFiles(x86)"], + "Microsoft/Edge/Application/msedge.exe" + ), + path.join( + process.env["LocalAppData"], + "Microsoft/Edge/Application/msedge.exe" + ), + ], + }; + } else if (platform === "darwin") { + paths = { + chrome: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"], + msedge: [ + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + ], + }; + } else if (platform === "linux") { + paths = { + chrome: [ + "/opt/google/chrome/chrome", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + ], + msedge: [ + "/opt/microsoft/msedge/msedge", + "/usr/bin/microsoft-edge", + "/usr/bin/microsoft-edge-stable", + ], + }; + } else { + throw new Error("不支持的操作系统"); + } + return paths[browser].find((p) => fs.existsSync(p)); +}; + +const isPortAvailable = (port) => { + return new Promise((resolve) => { + 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); + }); + }); +}; + +const waitForPort = async (port, timeout = 30000) => { + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + try { + await CDP.Version({ port }); + return true; + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + return false; +}; + +const findAvailablePort = async (startPort) => { + let port = startPort; + while (port < startPort + 100) { + const available = await isPortAvailable(port); + if (available) { + return port; + } + port++; + } + throw new Error("无法找到可用的调试端口"); +}; + +const startClient = async (options) => { + const { + browserType = "msedge", + useSingleUserDataDir = true, + proxy = null, + browserPath = getBrowserPath(browserType), + windowSize = null, + incognito = false, + headless = false, + disableExtensions = false, + } = options; + + if (!browserPath) { + throw new Error("未找到浏览器,或未指定浏览器路径"); + } + + const port = await findAvailablePort(9222); + setCurrentClientPort(port); + + const automationArgs = [ + `--remote-debugging-port=${port}`, + "--disable-infobars", + "--disable-notifications", + "--disable-popup-blocking", + "--disable-save-password-bubble", + "--disable-translate", + "--no-first-run", + "--no-default-browser-check", + "--user-data-start-with-quickcomposer", + ]; + + const incognitoArg = { + chrome: "--incognito", + msedge: "--inprivate", + }; + + const optionArgs = [ + windowSize ? `--window-size=${windowSize}` : "--start-maximized", + proxy ? `--proxy-server=${proxy}` : "", + incognito ? incognitoArg[browserType] : "", + headless ? "--headless" : "", + disableExtensions ? "--disable-extensions" : "", + useSingleUserDataDir + ? `--user-data-dir=${path.join( + os.tmpdir(), + `${browserType}-debug-${port}` + )}` + : "", + ].filter(Boolean); + + const args = [...automationArgs, ...optionArgs]; + + return new Promise(async (resolve, reject) => { + if (!useSingleUserDataDir) { + try { + await killRunningBrowser(browserType); + } catch (e) { + reject(e); + return; + } + } + const child = exec( + `"${browserPath}" ${args.join(" ")}`, + { windowsHide: true }, + async (error) => { + if (error) { + reject(error); + return; + } + } + ); + + waitForPort(port).then((success) => { + if (success) { + resolve({ pid: child.pid, port }); + } else { + reject(new Error("浏览器启动超时,请检查是否有权限问题或防火墙限制")); + } + }); + }); +}; + +const killRunningBrowser = (browserType = "msedge") => { + return new Promise((resolve, reject) => { + if (os.platform() === "win32") { + exec(`taskkill /F /IM ${browserType}.exe`, (error) => { + if (error) reject(error); + else resolve(); + }); + } else { + exec(`kill -9 $(pgrep ${browserType})`, (error) => { + if (error) reject(error); + else resolve(); + }); + } + }); +}; + +const destroyClientByPort = async (port) => { + const currentPort = await getCurrentClientPort(); + if (!port) { + port = currentPort; + } + try { + const client = await CDP({ port }); + await client.Browser.close(); + + if (port === currentPort) { + setCurrentClientPort(null); + } + } catch (error) { + throw new Error(`销毁客户端失败,请手动关闭`); + } +}; + +const switchClientByPort = async (port) => { + try { + const versionInfo = await CDP.Version({ port }); + if (!versionInfo) { + throw new Error(`端口 ${port} 未找到活动的浏览器实例`); + } + setCurrentClientPort(port); + } catch (error) { + throw new Error(`切换客户端失败: ${error.message}`); + } +}; + +const getClientPorts = async () => { + try { + // 创建所有端口检查的 Promise 数组 + const portChecks = []; + for (let port = 9222; port < 9322; port++) { + portChecks.push( + CDP.List({ port }) + .then(() => port) + .catch(() => 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 ports = await getClientPorts(); + if (!ports || ports.length === 0) { + throw new Error("未找到可用的浏览器实例,请先从实例管理里面启动新的实例"); + } + currentClientPort = ports[0]; + } + return currentClientPort; +}; + +const setCurrentClientPort = (port) => { + currentClientPort = port; +}; + + +module.exports = { + startClient, + destroyClientByPort, + switchClientByPort, + getClientPorts, + getCurrentClientPort, +}; diff --git a/plugin/lib/quickcomposer/browser/cookie.js b/plugin/lib/quickcomposer/browser/cookie.js new file mode 100644 index 0000000..254e589 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/cookie.js @@ -0,0 +1,40 @@ +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); + +const setCookie = async (tab, cookies, options = {}) => { + const target = await searchTarget(tab); + const { Network, Page } = await initCDP(target.id); + try { + const { frameTree } = await Page.getFrameTree(); + const url = frameTree.frame.url; + + for (const cookie of cookies) { + await Network.setCookie({ + name: cookie.name, + value: cookie.value, + domain: options.domain || url.split("/")[2], + path: options.path || "/", + secure: options.secure || false, + expires: options.expires + ? Math.floor(Date.now() / 1000) + options.expires * 3600 + : undefined, + }); + } + } finally { + await cleanupCDP(target.id); + } +}; + +const getCookie = async (tab, name) => { + const target = await searchTarget(tab); + const { Network } = await initCDP(target.id); + const { cookies } = await Network.getCookies(); + await cleanupCDP(target.id); + if (!name) return cookies; + return cookies.find((cookie) => cookie.name === name); +}; + +module.exports = { + setCookie, + getCookie, +}; diff --git a/plugin/lib/quickcomposer/browser/execScript.js b/plugin/lib/quickcomposer/browser/execScript.js index 4153fa0..b183ba3 100644 --- a/plugin/lib/quickcomposer/browser/execScript.js +++ b/plugin/lib/quickcomposer/browser/execScript.js @@ -1,5 +1,33 @@ -const { executeScript } = require("./browser"); const fs = require("fs"); +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); + +const executeScript = async (tab, script, args = {}) => { + const target = await searchTarget(tab); + try { + const { Runtime } = await initCDP(target.id); + const argNames = Object.keys(args); + const argValues = Object.values(args).map((v) => JSON.stringify(v)); + + const wrappedScript = ` + (async function(${argNames.join(", ")}) { + ${script} + })(${argValues.join(", ")}) + `; + + const { result } = await Runtime.evaluate({ + expression: wrappedScript, + returnByValue: true, + awaitPromise: true, + }); + + await cleanupCDP(target.id); + return result.value; + } catch (e) { + console.log(e); + throw new Error("执行脚本失败"); + } +}; const clickElement = async (tab, selector) => { return await executeScript( @@ -178,6 +206,7 @@ const injectLocalScript = async (tab, filePath) => { }; module.exports = { + executeScript, clickElement, inputText, submitForm, diff --git a/plugin/lib/quickcomposer/browser/getSelector.js b/plugin/lib/quickcomposer/browser/getSelector.js index 6a16eb5..6aafddc 100644 --- a/plugin/lib/quickcomposer/browser/getSelector.js +++ b/plugin/lib/quickcomposer/browser/getSelector.js @@ -1,4 +1,4 @@ -const { executeScript } = require("./browser"); +const { executeScript } = require("./execScript"); const fs = require("fs"); const path = require("path"); diff --git a/plugin/lib/quickcomposer/browser/index.js b/plugin/lib/quickcomposer/browser/index.js index 60b5507..a2c74f0 100644 --- a/plugin/lib/quickcomposer/browser/index.js +++ b/plugin/lib/quickcomposer/browser/index.js @@ -1,9 +1,17 @@ -const browser = require("./browser"); const getSelector = require("./getSelector"); const execScript = require("./execScript"); +const browserManager = require("./client"); +const tabs = require("./tabs"); +const url = require("./url"); +const cookie = require("./cookie"); +const screenshot = require("./screenshot"); module.exports = { - ...browser, + ...url, + ...tabs, ...getSelector, ...execScript, + ...browserManager, + ...cookie, + ...screenshot, }; diff --git a/plugin/lib/quickcomposer/browser/screenshot.js b/plugin/lib/quickcomposer/browser/screenshot.js new file mode 100644 index 0000000..56af39f --- /dev/null +++ b/plugin/lib/quickcomposer/browser/screenshot.js @@ -0,0 +1,84 @@ +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); +const fs = require("fs"); + +// 捕获标签页截图 +const captureScreenshot = async (tab, options = {}) => { + const target = await searchTarget(tab); + const { format = "png", quality = 100, savePath, selector = null } = options; + + try { + const { Page, DOM } = await initCDP(target.id); + await DOM.enable(); + + let clip = null; + if (selector) { + // 获取DOM节点 + const { root } = await DOM.getDocument(); + const { nodeId } = await DOM.querySelector({ + nodeId: root.nodeId, + selector: selector, + }); + + if (!nodeId) { + throw new Error(`未找到元素: ${selector}`); + } + + // 获取元素的精确四边形坐标 + const { quads } = await DOM.getContentQuads({ nodeId }); + if (!quads || quads.length === 0) { + throw new Error("无法获取元素位置信息"); + } + + // 获取布局指标 + const { visualViewport } = await Page.getLayoutMetrics(); + const { pageX, pageY } = visualViewport; + + // 计算边界框 + const quad = quads[0]; + const x = Math.min(quad[0], quad[2], quad[4], quad[6]); + const y = Math.min(quad[1], quad[3], quad[5], quad[7]); + const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; + const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; + + clip = { + x: Math.round(x - pageX), + y: Math.round(y - pageY), + width: Math.round(width), + height: Math.round(height), + scale: 1, + }; + + // 确保尺寸不为0 + if (clip.width === 0) clip.width = 1; + if (clip.height === 0) clip.height = 1; + } + + const screenshotParams = { + format, + quality: format === "jpeg" ? quality : undefined, + fromSurface: true, + captureBeyondViewport: !!selector, + }; + + if (clip) { + screenshotParams.clip = clip; + } + + const { data } = await Page.captureScreenshot(screenshotParams); + + await DOM.disable(); + + if (savePath) { + fs.writeFileSync(savePath, data, "base64"); + } + + return data; + } finally { + await cleanupCDP(target.id); + } +}; + +module.exports = { + captureScreenshot, +}; diff --git a/plugin/lib/quickcomposer/browser/tabs.js b/plugin/lib/quickcomposer/browser/tabs.js new file mode 100644 index 0000000..ce3e9bd --- /dev/null +++ b/plugin/lib/quickcomposer/browser/tabs.js @@ -0,0 +1,86 @@ +const { initCDP, cleanupCDP } = require("./cdp"); +const { getCurrentClientPort } = require("./client"); +const CDP = require("chrome-remote-interface"); + +const getTargets = async () => { + const port = await getCurrentClientPort(); + return await CDP.List({ port }); +}; + +const searchTarget = async (tab) => { + if (!tab || !tab.by || !tab.searchValue || tab.by === "active") { + const currentTab = await getCurrentTab(); + return currentTab; + } + + const targets = await getTargets(); + const target = targets.find((target) => + target[tab.by].includes(tab.searchValue) + ); + if (!target) { + throw new Error(`未找到目标标签页: ${tab.by} = ${tab.searchValue}`); + } + return target; +}; + +const getTabs = async () => { + const targets = await getTargets(); + return targets + .filter((target) => target.type === "page") + .map((target) => ({ + url: target.url, + title: target.title, + id: target.id, + })); +}; + +const getCurrentTab = async () => { + const targets = await getTargets(); + const currentTarget = targets.find((target) => target.type === "page"); + + if (!currentTarget) { + throw new Error("未找到当前活动标签页"); + } + + return { + url: currentTarget.url, + title: currentTarget.title, + id: currentTarget.id, + }; +}; + +const activateTab = async (tab) => { + const target = await searchTarget(tab); + 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); + const { targetId } = await Target.createTarget({ url }); + const { targetInfo } = await Target.getTargetInfo({ targetId }); + await cleanupCDP(currentTab.id); + return { + url: targetInfo.url, + title: targetInfo.title, + id: targetId, + }; +}; + +const closeTab = async (tab) => { + const target = await searchTarget(tab); + const port = await getCurrentClientPort(); + await cleanupCDP(target.id); + await CDP.Close({ id: target.id, port }); +}; + +module.exports = { + getTabs, + getCurrentTab, + activateTab, + createNewTab, + closeTab, + getTargets, + searchTarget, +}; diff --git a/plugin/lib/quickcomposer/browser/url.js b/plugin/lib/quickcomposer/browser/url.js new file mode 100644 index 0000000..596db30 --- /dev/null +++ b/plugin/lib/quickcomposer/browser/url.js @@ -0,0 +1,23 @@ +const { initCDP, cleanupCDP } = require("./cdp"); +const { searchTarget } = require("./tabs"); + +const getUrl = async (tab) => { + const target = await searchTarget(tab); + const { Page } = await initCDP(target.id); + const { frameTree } = await Page.getFrameTree(); + await cleanupCDP(target.id); + return frameTree.frame.url; +}; + +const setUrl = async (tab, url) => { + const target = await searchTarget(tab); + const { Page } = await initCDP(target.id); + await Page.navigate({ url }); + await Page.loadEventFired(); + await cleanupCDP(target.id); +}; + +module.exports = { + getUrl, + setUrl, +}; diff --git a/src/js/composer/commands/browserCommands.js b/src/js/composer/commands/browserCommands.js index 458f9c5..b0d8bdf 100644 --- a/src/js/composer/commands/browserCommands.js +++ b/src/js/composer/commands/browserCommands.js @@ -33,14 +33,14 @@ export const browserCommands = { defaultOpened: false, commands: [ { - value: "quickcomposer.browser.launchBrowser", + value: "quickcomposer.browser.startClient", label: "浏览器实例管理", icon: "launch", isAsync: true, config: [], subCommands: [ { - value: "quickcomposer.browser.launchBrowser", + value: "quickcomposer.browser.startClient", label: "启动浏览器实例", icon: "launch", config: [ @@ -118,11 +118,6 @@ export const browserCommands = { }, ], }, - { - value: "quickcomposer.browser.getClientPorts", - label: "获取所有浏览器实例端口", - icon: "list", - }, { value: "quickcomposer.browser.destroyClientByPort", label: "关闭浏览器实例", @@ -133,13 +128,18 @@ export const browserCommands = { component: "NumberInput", icon: "label", width: 12, - defaultValue: 9222, min: 9222, max: 9322, step: 1, + placeholder: "留空关闭当前操控的实例", }, ], }, + { + value: "quickcomposer.browser.getClientPorts", + label: "获取所有浏览器实例端口", + icon: "list", + }, { value: "quickcomposer.browser.getCurrentClientPort", label: "获取当前操控的实例端口",