mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-09 06:54:11 +08:00
479 lines
12 KiB
JavaScript
479 lines
12 KiB
JavaScript
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");
|
||
|
||
let clients = new Map(); // 存储每个标签页的CDP客户端
|
||
|
||
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 server = http.createServer();
|
||
server.listen(port, () => {
|
||
server.close(() => resolve(true));
|
||
});
|
||
server.on("error", () => resolve(false));
|
||
});
|
||
};
|
||
|
||
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;
|
||
} 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,
|
||
} = options;
|
||
|
||
if (!browserPath) {
|
||
throw new Error("未找到浏览器,或未指定浏览器路径");
|
||
}
|
||
|
||
const port = await findAvailablePort(9222);
|
||
|
||
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" : "",
|
||
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 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(`请先通过浏览器控制中的"启动浏览器"打开浏览器`);
|
||
}
|
||
}
|
||
return clients.get(targetId);
|
||
};
|
||
|
||
const cleanupCDP = async (targetId) => {
|
||
const client = clients.get(targetId);
|
||
if (client) {
|
||
await client.client.close();
|
||
clients.delete(targetId);
|
||
}
|
||
};
|
||
|
||
// 获取所有标签页
|
||
const getTabs = async () => {
|
||
const targets = await CDP.List();
|
||
return targets
|
||
.filter((target) => target.type === "page")
|
||
.map((target) => ({
|
||
url: target.url,
|
||
title: target.title,
|
||
id: target.id,
|
||
}));
|
||
};
|
||
|
||
// 获取当前活动标签页
|
||
const getCurrentTab = async () => {
|
||
const targets = await CDP.List();
|
||
// 一般排第一个的就是活动标签页
|
||
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 CDP.List();
|
||
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);
|
||
await CDP.Activate({ id: target.id });
|
||
};
|
||
|
||
// 创建新标签页
|
||
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);
|
||
await cleanupCDP(target.id);
|
||
await CDP.Close({ id: target.id });
|
||
};
|
||
|
||
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 = `
|
||
(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 {
|
||
// 直接从Page获取URL,避免创建新连接
|
||
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, Emulation, 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 = {
|
||
launchBrowser,
|
||
killRunningBrowser,
|
||
getTabs,
|
||
getCurrentTab,
|
||
activateTab,
|
||
createNewTab,
|
||
closeTab,
|
||
getUrl,
|
||
setUrl,
|
||
executeScript,
|
||
setCookie,
|
||
getCookie,
|
||
captureScreenshot,
|
||
};
|