mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-09 15:04:06 +08:00
401 lines
9.9 KiB
JavaScript
401 lines
9.9 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 client = null;
|
|
let Page = null;
|
|
let Runtime = null;
|
|
let DOM = 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 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,
|
|
headless = false,
|
|
proxy = null,
|
|
browserPath = getBrowserPath(browserType),
|
|
windowSize = null,
|
|
} = options;
|
|
|
|
if (!browserPath) {
|
|
throw new Error("未找到浏览器,或未指定浏览器路径");
|
|
}
|
|
|
|
// 查找可用端口
|
|
const port = await findAvailablePort(9222);
|
|
|
|
const args = [
|
|
`--remote-debugging-port=${port}`,
|
|
"--no-first-run",
|
|
"--no-default-browser-check",
|
|
"--start-maximized",
|
|
headless ? "--headless" : "",
|
|
windowSize ? `--window-size=${windowSize}` : "",
|
|
proxy ? `--proxy-server=${proxy}` : "",
|
|
useSingleUserDataDir
|
|
? `--user-data-dir=${path.join(
|
|
os.tmpdir(),
|
|
`${browserType}-debug-${port}`
|
|
)}`
|
|
: "",
|
|
].filter(Boolean);
|
|
|
|
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 (port) => {
|
|
if (!client) {
|
|
try {
|
|
client = await CDP({ port });
|
|
({ Page, Runtime, DOM } = client);
|
|
await Promise.all([Page.enable(), Runtime.enable(), DOM.enable()]);
|
|
} catch (err) {
|
|
console.log(err);
|
|
throw new Error(
|
|
`请先通过浏览器控制中的“启动浏览器”打开浏览器`
|
|
);
|
|
}
|
|
}
|
|
return { Page, Runtime, DOM };
|
|
};
|
|
|
|
const getUrl = async () => {
|
|
const { Page } = await initCDP();
|
|
const { frameTree } = await Page.getFrameTree();
|
|
return frameTree.frame.url;
|
|
};
|
|
|
|
const setUrl = async (url) => {
|
|
const { Page } = await initCDP();
|
|
await Page.navigate({ url });
|
|
await Page.loadEventFired();
|
|
};
|
|
|
|
const executeScript = async (script, args = {}) => {
|
|
const { Runtime } = await initCDP();
|
|
// 构建参数列表
|
|
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,
|
|
});
|
|
return result.value;
|
|
};
|
|
|
|
const getTabs = async () => {
|
|
const targets = await CDP.List();
|
|
return targets
|
|
.filter((target) => target.type === "page")
|
|
.map((target) => ({
|
|
url: target.url,
|
|
title: target.title,
|
|
}));
|
|
};
|
|
|
|
const activateTab = async (index) => {
|
|
const targets = await CDP.List();
|
|
const pages = targets.filter((target) => target.type === "page");
|
|
if (index > 0 && index <= pages.length) {
|
|
const targetId = pages[index - 1].id;
|
|
await CDP.Activate({ id: targetId });
|
|
}
|
|
};
|
|
|
|
const clickElement = async (selector) => {
|
|
return await executeScript(`document.querySelector('${selector}').click()`);
|
|
};
|
|
|
|
const inputText = async (selector, text) => {
|
|
return await executeScript(
|
|
`
|
|
const el = document.querySelector('${selector}');
|
|
el.value = '${text}';
|
|
el.dispatchEvent(new Event('input'));
|
|
el.dispatchEvent(new Event('change'));
|
|
`
|
|
);
|
|
};
|
|
|
|
const getText = async (selector) => {
|
|
return await executeScript(
|
|
`document.querySelector('${selector}')?.textContent || ''`
|
|
);
|
|
};
|
|
|
|
const getHtml = async (selector) => {
|
|
return await executeScript(
|
|
`const element = document.querySelector('${selector}');
|
|
return element ? element.innerHTML : '';`
|
|
);
|
|
};
|
|
|
|
const hideElement = async (selector) => {
|
|
return await executeScript(
|
|
`document.querySelector('${selector}').style.display = 'none'`
|
|
);
|
|
};
|
|
|
|
const showElement = async (selector) => {
|
|
return await executeScript(
|
|
`document.querySelector('${selector}').style.display = ''`
|
|
);
|
|
};
|
|
|
|
const injectCSS = async (css) => {
|
|
return await executeScript(
|
|
`
|
|
const style = document.createElement('style');
|
|
style.textContent = \`${css}\`;
|
|
document.head.appendChild(style);
|
|
`
|
|
);
|
|
};
|
|
|
|
const setCookie = async (cookies, options = {}) => {
|
|
const { Network } = await initCDP();
|
|
for (const cookie of cookies) {
|
|
await Network.setCookie({
|
|
name: cookie.name,
|
|
value: cookie.value,
|
|
domain: options.domain || undefined,
|
|
path: options.path || "/",
|
|
secure: options.secure || false,
|
|
expires: options.expires
|
|
? Math.floor(Date.now() / 1000) + options.expires * 3600
|
|
: undefined,
|
|
});
|
|
}
|
|
};
|
|
|
|
const getCookie = async (name) => {
|
|
const { Network } = await initCDP();
|
|
const cookies = await Network.getCookies();
|
|
return cookies.find((cookie) => cookie.name === name);
|
|
};
|
|
|
|
const scrollTo = async (x, y) => {
|
|
return await executeScript(`window.scrollTo(${x}, ${y})`);
|
|
};
|
|
|
|
const scrollToElement = async (selector) => {
|
|
return await executeScript(
|
|
`document.querySelector('${selector}').scrollIntoView()`
|
|
);
|
|
};
|
|
|
|
const getScrollPosition = async () => {
|
|
return await executeScript(`
|
|
return JSON.stringify({
|
|
x: window.pageXOffset || document.documentElement.scrollLeft,
|
|
y: window.pageYOffset || document.documentElement.scrollTop
|
|
});
|
|
`);
|
|
};
|
|
|
|
const getPageSize = async () => {
|
|
return await executeScript(`
|
|
return JSON.stringify({
|
|
width: Math.max(
|
|
document.documentElement.scrollWidth,
|
|
document.documentElement.clientWidth
|
|
),
|
|
height: Math.max(
|
|
document.documentElement.scrollHeight,
|
|
document.documentElement.clientHeight
|
|
)
|
|
});
|
|
`);
|
|
};
|
|
|
|
const waitForElement = async (selector, timeout = 5000) => {
|
|
const startTime = Date.now();
|
|
while (Date.now() - startTime < timeout) {
|
|
const result = await executeScript(
|
|
`!!document.querySelector('${selector}')`
|
|
);
|
|
if (result) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
throw new Error(`等待元素 ${selector} 超时`);
|
|
};
|
|
|
|
module.exports = {
|
|
launchBrowser,
|
|
getUrl,
|
|
setUrl,
|
|
executeScript,
|
|
getTabs,
|
|
activateTab,
|
|
clickElement,
|
|
inputText,
|
|
getText,
|
|
getHtml,
|
|
hideElement,
|
|
showElement,
|
|
injectCSS,
|
|
setCookie,
|
|
getCookie,
|
|
scrollTo,
|
|
scrollToElement,
|
|
getScrollPosition,
|
|
getPageSize,
|
|
waitForElement,
|
|
};
|