feat: 浏览器自动化操作开发

This commit is contained in:
DEV_DSW
2026-03-03 17:05:13 +08:00
parent d99f1dd98e
commit 20e825215f
18 changed files with 495 additions and 68 deletions

View File

@@ -53,8 +53,8 @@ export enum IPC_EVENTS {
IS_DARK_THEME = 'is-dark-theme',
THEME_MODE_UPDATED = 'theme-mode-updated',
// 任务操作
TASK_OPERATION = 'task-operation',
// 执行脚本
EXECUTE_SCRIPT = 'execute-script',
}
export const MAIN_WIN_SIZE = {

View File

@@ -3,21 +3,23 @@ import { CONFIG_KEYS } from '@common/constants'
import { setupMainWindow } from './wins';
import started from 'electron-squirrel-startup'
import configManager from '@main/service/config-service'
import logManager from '@main/service/logger'
import { runTaskOperationService } from '@main/process/runTaskOperationService'
import log from 'electron-log';
// import logManager from '@main/service/logger'
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit();
}
process.on('uncaughtException', (err) => {
logManager.error('uncaughtException', err);
});
// process.on('uncaughtException', (err) => {
// logManager.error('uncaughtException', err);
// });
process.on('unhandledRejection', (reason, promise) => {
logManager.error('unhandledRejection', reason, promise);
});
// process.on('unhandledRejection', (reason, promise) => {
// logManager.error('unhandledRejection', reason, promise);
// });
app.whenReady().then(() => {
setupMainWindow();
@@ -33,7 +35,7 @@ app.whenReady().then(() => {
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin' && !configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY)) {
logManager.info('app closing due to all windows being closed');
log.info('app closing due to all windows being closed');
app.quit();
}
});

View File

@@ -1,2 +1,80 @@
export function runTaskOperationService() {}
import { ipcMain } from 'electron';
import { IPC_EVENTS } from '@common/constants';
import { launchLocalChrome } from '@main/utils/chrome/launchLocalChrome'
import { executeScriptService } from '@main/service/execute-script-service';
import path from 'path'
export function runTaskOperationService() {
const executeScriptServiceInstance = new executeScriptService();
ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (_event, options: any) => {
try {
/**
* options参数包括房型、日期范围
* 房型:亲子房、雅致套房
* 日期范围2023-10-01至2023-10-07
* 备注:操作的渠道有:飞猪、美团、抖音来客
* 这里需要一个排队任务,用队列来处理各渠道的操作路径自动化,先将任务加入队列,然后按顺序执行脚本
*/
await launchLocalChrome(options)
const result = await executeScriptServiceInstance.executeScript(path.join(__dirname, '../../scripts/fg_trace.js'), options)
return { success: true, result };
} catch (error: any) {
return { success: false, error: error.message };
}
});
}
// export function runTaskOperationService() {
// ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (event, params) => {
// logManager.info('Received task operation:', params);
// return new Promise((resolve, reject) => {
// // 脚本路径
// const scriptPath = path.join(__dirname, '../../scripts/fg_trace.js');
// const child = fork(scriptPath, [], {
// stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
// env: { ...process.env, ...params } // 传递环境变量
// });
// let output = '';
// let errorOutput = '';
// if (child.stdout) {
// child.stdout.on('data', (data) => {
// const msg = data.toString();
// logManager.info(`[Task Script]: ${msg}`);
// output += msg;
// });
// }
// if (child.stderr) {
// child.stderr.on('data', (data) => {
// const msg = data.toString();
// logManager.error(`[Task Script Error]: ${msg}`);
// errorOutput += msg;
// });
// }
// child.on('close', (code) => {
// logManager.info(`Task script exited with code ${code}`);
// if (code === 0) {
// resolve({ success: true, output });
// } else {
// // 如果是因为模块找不到或语法错误退出,这里会捕获
// reject(new Error(`Script exited with code ${code}. Error: ${errorOutput}`));
// }
// });
// child.on('error', (err) => {
// logManager.error('Failed to start task script:', err);
// reject(err);
// });
// });
// });
// }

View File

@@ -1,39 +1,10 @@
import { chromium } from 'playwright';
import { checkLoginStatus } from '@utils/checkLoginStatus';
import dotenv from 'dotenv';
import log from 'electron-log';
dotenv.config();
const checkLoginStatus = async (page) => {
try {
const currentUrl = await page.url();
log.info('current url==========>:', currentUrl);
const loginPagePatterns = ['/login', '/signin', '/auth'];
const isLoginPage = loginPagePatterns.some(pattern => currentUrl.includes(pattern));
if(!isLoginPage) {
return true;
}
// 检查页面内容中的关键字
const pageContent = await page.content();
const loginKeywords = ['退出', '房价房量管理'];
const hasLoginKeyword = loginKeywords.some(keyword =>
pageContent.includes(keyword)
);
if (hasLoginKeyword) {
return true;
}
return false;
} catch (error) {
return false;
}
}
(async () => {
const browser = await chromium.connectOverCDP('http://localhost:9222');
const context = browser.contexts()[0];
@@ -116,17 +87,17 @@ const checkLoginStatus = async (page) => {
* 2筛选日期下存在没有安排的房型
*/
await page.getByRole('textbox', { name: '-02-27' }).click();
await page.getByText('27').nth(3).click();
// await page.getByRole('textbox', { name: '-02-27' }).click();
// await page.getByText('27').nth(3).click();
// 开启房型div:nth-child(动态变化的) > .boardRow___p2ZiO > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r
await page.locator('div:nth-child(7) > div > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').first().click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(2) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(3) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(4) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(5) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
// await page.locator('div:nth-child(7) > div > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').first().click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(2) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(3) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(4) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(5) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
// 关闭房型div:nth-child(动态变化的) > .boardRow___p2ZiO > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').first().click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(2) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(3) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(4) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').first().click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(2) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(3) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
// await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(4) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
})();

View File

@@ -0,0 +1,38 @@
import { EventEmitter } from 'events';
import { spawn } from 'child_process';
import log from 'electron-log';
export class executeScriptService extends EventEmitter {
// 执行脚本
async executeScript(scriptPath: string, options: object): Promise<{ success: boolean; message?: string; error?: string }> {
try {
const child = spawn('node', [scriptPath], {
env: {
...process.env,
...options,
}
});
child.stdout.on('data', (data: Buffer) => {
log.info(`stdout: ${data.toString()}`);
});
child.stderr.on('data', (data: Buffer) => {
log.info(`stderr: ${data.toString()}`);
});
child.on('close', (code: number) => {
log.info(`子进程退出,退出码 ${code}`);
});
return { success: true, message: 'Node 脚本执行中' };
} catch (error) {
return { success: false, message: '运行 Node 脚本时出错' };
}
}
}

View File

@@ -0,0 +1,31 @@
import log from 'electron-log';
export const checkLoginStatus = async (page: any) => {
try {
const currentUrl = await page.url();
log.info('current url==========>:', currentUrl);
const loginPagePatterns = ['/login', '/signin', '/auth'];
const isLoginPage = loginPagePatterns.some(pattern => currentUrl.includes(pattern));
if(!isLoginPage) {
return true;
}
// 检查页面内容中的关键字
const pageContent = await page.content();
const loginKeywords = ['退出', '房价房量管理'];
const hasLoginKeyword = loginKeywords.some(keyword =>
pageContent.includes(keyword)
);
if (hasLoginKeyword) {
return true;
}
return false;
} catch (error) {
return false;
}
}

View File

@@ -0,0 +1,17 @@
// 启动本地Chrome
export function getChromePath () {
if (process.platform === 'win32') {
// "C:\Program Files\Google\Chrome\Application\chrome.exe"
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
}
if (process.platform === 'darwin') {
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
}
if (process.platform === 'linux') {
return 'google-chrome';
}
}

View File

@@ -0,0 +1,7 @@
import path from "node:path";
import { app } from 'electron';
// 多账号隔离
export function getProfileDir (accountId: string) {
return path.join(app.getPath('userData'), `profiles`, accountId);
}

View File

@@ -0,0 +1,21 @@
import http from 'http';
// Chrome是否已运行
export async function isChromeRunning (): Promise<boolean> {
try {
return new Promise((resolve) => {
const req = http.get('http://localhost:9222/json/version', (res: any) => {
resolve(res.statusCode === 200);
});
req.on('error', () => resolve(false));
req.setTimeout(1000, () => {
req.destroy();
resolve(false);
});
});
} catch (error) {
return false
}
}

View File

@@ -0,0 +1,17 @@
import net from 'net';
// 检查端口占用
export function isPortInUse (port: number) {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', (err: any) => resolve(true));
server.once('listening', () => {
server.close();
resolve(false);
});
server.listen(port);
});
}

View File

@@ -0,0 +1,48 @@
import { getChromePath } from './getChromePath';
import { getProfileDir } from './getProfileDir';
import { isPortInUse } from './isPortInUse';
import { isChromeRunning } from './isChromeRunning';
import { spawn } from 'child_process';
import log from 'electron-log';
// 启动本地浏览器
export async function launchLocalChrome (options: any) {
const chromePath = getChromePath();
// 多账号隔离
// const profileDir = getProfileDir(accountId);
log.info(`Launching Chrome with user data dir: ${options}`);
// 检查端口是否被占用
const portInUse = await isPortInUse(9222);
if (portInUse) {
log.info('Chrome already running on port 9222, skip launching.');
return;
}
if (await isChromeRunning()) {
log.info('Chrome already running, skip launching.');
return;
}
return new Promise((resolve, reject) => {
const chromeProcess = spawn(chromePath as string, [
'--remote-debugging-port=9222',
'--window-size=1920,1080',
'--window-position=0,0',
'--no-first-run',
'--no-default-browser-check'
// `--user-data-dir=${profileDir}`,
// '--window-maximized',
], {
detached: true,
stdio: 'ignore'
});
chromeProcess.on('error', reject);
// 等浏览器起来
resolve(0);
});
}

View File

@@ -56,8 +56,8 @@ const api: WindowApi = {
error: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta),
},
// 任务操作
taskOperation: (params: any) => ipcRenderer.invoke(IPC_EVENTS.TASK_OPERATION, params),
// 执行脚本
executeScript: (params: any) => ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params),
}
contextBridge.exposeInMainWorld('api', api)

View File

@@ -8,7 +8,7 @@
</el-select>
</el-form-item>
<el-form-item label="选择日期" prop="date">
<el-date-picker v-model="form.date" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期"
<el-date-picker v-model="ranger" type="daterange" value-format="YYYY-MM-DD" placeholder="请选择日期"
style="width: 100%">
</el-date-picker>
</el-form-item>
@@ -29,17 +29,19 @@ const isVisible = ref(false)
const title = ref('')
const form = ref({
roomType: '',
date: '',
startTime: '',
endTime: '',
operation: '',
})
const rules = ref({
roomType: [
{ required: true, message: '请选择房型', trigger: 'blur' },
],
date: [
{ required: true, message: '请选择日期', trigger: 'blur' },
ranger: [
{ required: true, message: '请选择日期范围', trigger: 'blur' },
],
})
const ranger = ref([])
// 打开弹窗
const open = ({ type }: taskCenterItem) => {
@@ -56,7 +58,9 @@ const close = () => {
// 重置form
const reset = () => {
form.value.roomType = ''
form.value.date = ''
form.value.startTime = ''
form.value.endTime = ''
ranger.value = []
}
// 取消操作
@@ -68,8 +72,13 @@ const cancel = () => {
// 确认操作
const confirm = () => {
close()
form.value.startTime = ranger.value[0]
form.value.endTime = ranger.value[1]
console.log(form.value)
window.api.taskOperation(form.value)
/**
* 坑传给进程的参数不能是ref包裹的reactive对象
*/
window.api.executeScript({ ...form.value })
}
defineExpose({