mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-29 12:22:44 +08:00
重构浏览器自动化后端代码结构
This commit is contained in:
parent
df8c6c48f8
commit
179c0c567f
@ -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,
|
|
||||||
};
|
|
44
plugin/lib/quickcomposer/browser/cdp.js
Normal file
44
plugin/lib/quickcomposer/browser/cdp.js
Normal file
@ -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,
|
||||||
|
};
|
286
plugin/lib/quickcomposer/browser/client.js
Normal file
286
plugin/lib/quickcomposer/browser/client.js
Normal file
@ -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,
|
||||||
|
};
|
40
plugin/lib/quickcomposer/browser/cookie.js
Normal file
40
plugin/lib/quickcomposer/browser/cookie.js
Normal file
@ -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,
|
||||||
|
};
|
@ -1,5 +1,33 @@
|
|||||||
const { executeScript } = require("./browser");
|
|
||||||
const fs = require("fs");
|
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) => {
|
const clickElement = async (tab, selector) => {
|
||||||
return await executeScript(
|
return await executeScript(
|
||||||
@ -178,6 +206,7 @@ const injectLocalScript = async (tab, filePath) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
executeScript,
|
||||||
clickElement,
|
clickElement,
|
||||||
inputText,
|
inputText,
|
||||||
submitForm,
|
submitForm,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { executeScript } = require("./browser");
|
const { executeScript } = require("./execScript");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
const browser = require("./browser");
|
|
||||||
const getSelector = require("./getSelector");
|
const getSelector = require("./getSelector");
|
||||||
const execScript = require("./execScript");
|
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 = {
|
module.exports = {
|
||||||
...browser,
|
...url,
|
||||||
|
...tabs,
|
||||||
...getSelector,
|
...getSelector,
|
||||||
...execScript,
|
...execScript,
|
||||||
|
...browserManager,
|
||||||
|
...cookie,
|
||||||
|
...screenshot,
|
||||||
};
|
};
|
||||||
|
84
plugin/lib/quickcomposer/browser/screenshot.js
Normal file
84
plugin/lib/quickcomposer/browser/screenshot.js
Normal file
@ -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,
|
||||||
|
};
|
86
plugin/lib/quickcomposer/browser/tabs.js
Normal file
86
plugin/lib/quickcomposer/browser/tabs.js
Normal file
@ -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,
|
||||||
|
};
|
23
plugin/lib/quickcomposer/browser/url.js
Normal file
23
plugin/lib/quickcomposer/browser/url.js
Normal file
@ -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,
|
||||||
|
};
|
@ -33,14 +33,14 @@ export const browserCommands = {
|
|||||||
defaultOpened: false,
|
defaultOpened: false,
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
value: "quickcomposer.browser.launchBrowser",
|
value: "quickcomposer.browser.startClient",
|
||||||
label: "浏览器实例管理",
|
label: "浏览器实例管理",
|
||||||
icon: "launch",
|
icon: "launch",
|
||||||
isAsync: true,
|
isAsync: true,
|
||||||
config: [],
|
config: [],
|
||||||
subCommands: [
|
subCommands: [
|
||||||
{
|
{
|
||||||
value: "quickcomposer.browser.launchBrowser",
|
value: "quickcomposer.browser.startClient",
|
||||||
label: "启动浏览器实例",
|
label: "启动浏览器实例",
|
||||||
icon: "launch",
|
icon: "launch",
|
||||||
config: [
|
config: [
|
||||||
@ -118,11 +118,6 @@ export const browserCommands = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "quickcomposer.browser.getClientPorts",
|
|
||||||
label: "获取所有浏览器实例端口",
|
|
||||||
icon: "list",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "quickcomposer.browser.destroyClientByPort",
|
value: "quickcomposer.browser.destroyClientByPort",
|
||||||
label: "关闭浏览器实例",
|
label: "关闭浏览器实例",
|
||||||
@ -133,13 +128,18 @@ export const browserCommands = {
|
|||||||
component: "NumberInput",
|
component: "NumberInput",
|
||||||
icon: "label",
|
icon: "label",
|
||||||
width: 12,
|
width: 12,
|
||||||
defaultValue: 9222,
|
|
||||||
min: 9222,
|
min: 9222,
|
||||||
max: 9322,
|
max: 9322,
|
||||||
step: 1,
|
step: 1,
|
||||||
|
placeholder: "留空关闭当前操控的实例",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "quickcomposer.browser.getClientPorts",
|
||||||
|
label: "获取所有浏览器实例端口",
|
||||||
|
icon: "list",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "quickcomposer.browser.getCurrentClientPort",
|
value: "quickcomposer.browser.getCurrentClientPort",
|
||||||
label: "获取当前操控的实例端口",
|
label: "获取当前操控的实例端口",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user