diff --git a/global.d.ts b/global.d.ts index 6866735..bc65f3f 100644 --- a/global.d.ts +++ b/global.d.ts @@ -82,6 +82,8 @@ declare global { }, // 执行脚本 executeScript: (options: any) => Promise<{success: boolean, error?: string}>, + // 打开渠道 + openChannel: (channels: any) => Promise<{success: boolean, error?: string}>, } interface Window { diff --git a/src/common/constants.ts b/src/common/constants.ts index dfa3050..065d587 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -55,6 +55,9 @@ export enum IPC_EVENTS { // 执行脚本 EXECUTE_SCRIPT = 'execute-script', + + // 打开渠道 + OPEN_CHANNEL = 'open-channel', } export const MAIN_WIN_SIZE = { diff --git a/src/main/process/runTaskOperationService.ts b/src/main/process/runTaskOperationService.ts index 408b77e..52cef49 100644 --- a/src/main/process/runTaskOperationService.ts +++ b/src/main/process/runTaskOperationService.ts @@ -3,17 +3,74 @@ import { ipcMain, app } from 'electron'; import { IPC_EVENTS } from '@common/constants'; import { launchLocalChrome } from '@main/utils/chrome/launchLocalChrome' import { executeScriptService } from '@main/service/execute-script-service'; +import { chromium } from 'playwright'; +import { addStealthInit, connectCdpContext, safeDisconnectBrowser } from '@main/utils/chrome/cdp' import fs from 'fs' import path from 'path' import log from 'electron-log'; +const openedTabIndexByChannelName = new Map() + export function runTaskOperationService() { const executeScriptServiceInstance = new executeScriptService(); + // 打开渠道 + ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels: any) => { + try { + await launchLocalChrome() + const arr = Array.isArray(channels) ? channels : [] + const keys = arr + .map((it: any) => typeof it === 'string' ? it : it.channelName) + .filter(Boolean) + if (keys.length === 0) return { success: true } + + const urlMap: Record = {} + + for (const it of arr) { + const name = typeof it === 'string' ? it : it.channelName + const url = typeof it === 'string' ? undefined : it?.channelUrl + if (name && url) urlMap[name] = url + } + + const { browser, context } = await connectCdpContext(chromium) + await addStealthInit(context) + + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const targetUrl = urlMap[key] + + let pages = await context.pages() + + while (pages.length <= i) { + await context.newPage() + pages = await context.pages() + } + + const page = pages[i] + await page.bringToFront() + + if (targetUrl) { + const current = page.url() + + if (!current || !current.startsWith(targetUrl)) { + await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any }) + } + } + + openedTabIndexByChannelName.set(key, i) + } + + await safeDisconnectBrowser(browser) + + return { success: true } + } catch (error) { + return { success: false, error: (error as any)?.message || 'open channel failed' } + } + }) + + // 执行脚本 ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (_event, options: any) => { try { - await launchLocalChrome(options) - // 从options.roomList列表中找到对应的名称 const roomType = options.roomList.find((item: any) => item.id === options.roomType); @@ -44,13 +101,21 @@ export function runTaskOperationService() { const results: any[] = [] for (let i = 0; i < scriptPaths.length; i++) { const item = scriptPaths[i] + const channelNameMap: Record = { + fzName: 'fliggy', + mtName: 'meituan', + dyHotelName: 'douyin', + dyHotSpringName: 'douyin', + } + const mappedName = channelNameMap[item.channel] + const tabIndex = mappedName ? (openedTabIndexByChannelName.get(mappedName) ?? i) : i log.info(`Launching script for channel ${item.channel}: ${item.scriptPath}`) const result = await executeScriptServiceInstance.executeScript(item.scriptPath, { roomType: roomType[item.channel], startTime: options.startTime, endTime: options.endTime, operation: options.operation, - tabIndex: i, + tabIndex, }) results.push({ channel: item.channel, diff --git a/src/main/scripts/common/tabs.js b/src/main/scripts/common/tabs.js new file mode 100644 index 0000000..ed043b5 --- /dev/null +++ b/src/main/scripts/common/tabs.js @@ -0,0 +1,39 @@ +const preparePage = async (chromium, { tabIndex = Number(process.env.TAB_INDEX), endpoint = process.env.CDP_ENDPOINT || 'http://127.0.0.1:9222' } = {}) => { + const browser = await chromium.connectOverCDP(endpoint); + const context = browser.contexts()[0]; + + if (!context) { + throw new Error('No browser context available'); + } + + await context.addInitScript(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + }); + + const idx = Number.isFinite(tabIndex) && tabIndex >= 0 ? Math.floor(tabIndex) : 0; + let pages = await context.pages(); + while (pages.length <= idx) { + await context.newPage(); + pages = await context.pages(); + } + const page = pages[idx]; + await page.bringToFront(); + + return { browser, context, page }; +}; + +const safeDisconnectBrowser = async (browser) => { + if (!browser) return; + try { + if (typeof browser.disconnect === 'function') { + await browser.disconnect(); + } else { + await browser.close(); + } + } catch {} +}; + +module.exports = { + preparePage, + safeDisconnectBrowser, +}; diff --git a/src/main/scripts/fg_trace.mjs b/src/main/scripts/fg_trace.mjs index 630bc7d..f949eec 100644 --- a/src/main/scripts/fg_trace.mjs +++ b/src/main/scripts/fg_trace.mjs @@ -1,8 +1,8 @@ import { chromium } from 'playwright'; import log from 'electron-log'; -import checkLoginStatusPkg from './common/checkLoginStatus.js'; +import tabsPkg from './common/tabs.js'; -const { checkLoginStatus } = checkLoginStatusPkg; +const { preparePage, safeDisconnectBrowser } = tabsPkg; const parseDateInput = (dateStr) => { if (!dateStr) return null; @@ -61,7 +61,7 @@ const findHeaderDateIndex = async (container, mmdd) => { return -1; }; -const toggleRoomByDateIndex = async (page, container, { roomType, dateIndex, operation }) => { +const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation }) => { const roomAnchor = container.getByText(roomType, { exact: true }).first(); await roomAnchor.scrollIntoViewIfNeeded(); @@ -91,70 +91,17 @@ const toggleRoomByDateIndex = async (page, container, { roomType, dateIndex, ope }; (async () => { - const browser = await chromium.connectOverCDP('http://127.0.0.1:9222'); + let browser; try { - const context = browser.contexts()[0]; + const prepared = await preparePage(chromium); + browser = prepared.browser; + const page = prepared.page; - await context.addInitScript(() => { - Object.defineProperty(navigator, 'webdriver', { get: ()=> undefined }); - }); - - const pages = await context.pages(); - const tabIndex = Number(process.env.TAB_INDEX); - const page = pages.length ? pages[tabIndex] : await context.newPage(); - - await page.goto('https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1'); - - const isLogin = await checkLoginStatus(page); - - if(!isLogin) { - await page.getByRole('textbox', { name: '请输入账号' }).dblclick(); - await page.getByRole('textbox', { name: '请输入账号' }).click(); - await page.getByRole('textbox', { name: '请输入账号' }).fill('hoteltmwq123', { delay: 80 + Math.random() * 120 }); - await page.waitForTimeout(1000 + Math.random() * 1000); - await page.getByRole('button', { name: '下一步' }).click(); - - const frame_1 = await page.frameLocator('#alibaba-login-box'); - await page.locator('#alibaba-login-box').contentFrame().getByRole('textbox', { name: '请输入登录密码' }).dblclick(); - await page.locator('#alibaba-login-box').contentFrame().getByRole('textbox', { name: '请输入登录密码' }).fill('Tmwq654321', { delay: 80 + Math.random() * 120 }); - await page.waitForTimeout(1000 + Math.random() * 1000); - await page.locator('#alibaba-login-box').contentFrame().getByRole('button', { name: '登录' }).click(); - - // 等待滑块真正出现在 DOM 并可见 - await page.waitForTimeout(4000 + Math.random() * 1000); - const frame_2 = await frame_1.frameLocator('#baxia-dialog-content'); - const container = await frame_2.locator('#nc_1_nocaptcha'); - const slider = await frame_2.locator('#nc_1_n1z'); - const isVisible = await slider.isVisible(); - - if (isVisible) { - // 重新获取滑块按钮(可能嵌套在 iframe 里) - const containerBox = await container.boundingBox(); - const sliderBox = await slider.boundingBox(); - - const startX = sliderBox.x + sliderBox.width / 2; - const startY = sliderBox.y + sliderBox.height / 2; - const distance = containerBox.width - sliderBox.width; // 适当拉长拖动距离 - const steps = 20; // 分多步模拟人手拖动 - - await page.mouse.move(startX, startY); - // 等待随机时间再开始滑动(模拟人类反应) - await page.waitForTimeout(200 + Math.random() * 300); - await page.mouse.down(); - await page.waitForTimeout(100 + Math.random() * 200); - - // 按轨迹滑动 - for (let i = 0; i < steps; i++) { - await page.mouse.move( - sliderBox.x + sliderBox.width / 2 + (distance * (i + 1) / steps), - sliderBox.y + sliderBox.height / 2 + (Math.random() * 5 - 2.5), // 模拟轻微Y轴抖动 - { steps: 5 } - ); - } - - await page.mouse.up(); - } + const targetUrl = 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1'; + const currentUrl = page.url(); + if (!currentUrl || !currentUrl.startsWith(targetUrl)) { + await page.goto(targetUrl); } await page.waitForTimeout(4000 + Math.random() * 300); @@ -182,21 +129,13 @@ const toggleRoomByDateIndex = async (page, container, { roomType, dateIndex, ope if (dateIndex < 0) { throw new Error(`Date not found in header: ${mmdd}`); } - await toggleRoomByDateIndex(page, container, { roomType, dateIndex, operation }); + await toggleRoomByDateIndex(container, { roomType, dateIndex, operation }); await page.waitForTimeout(600 + Math.random() * 600); } } catch (error) { log.error(error); process.exitCode = 1; }finally { - if (browser) { - try { - if (typeof browser.disconnect === 'function') { - await browser.disconnect(); - } else { - await browser.close(); - } - } catch {} - } + await safeDisconnectBrowser(browser); } })(); diff --git a/src/main/scripts/mt_trace.mjs b/src/main/scripts/mt_trace.mjs index 5979e18..6ddd4b7 100644 --- a/src/main/scripts/mt_trace.mjs +++ b/src/main/scripts/mt_trace.mjs @@ -1,48 +1,24 @@ import { chromium } from 'playwright'; import log from 'electron-log'; - -const getOrCreateTab = async (context, targetIndex) => { - const idx = - Number.isFinite(targetIndex) && targetIndex >= 0 ? Math.floor(targetIndex) : 1; - - let pages = await context.pages(); - while (pages.length <= idx) { - await context.newPage(); - pages = await context.pages(); - } - const page = pages[idx]; - await page.bringToFront(); - return page; -}; +import tabsPkg from './common/tabs.js'; +const { preparePage, safeDisconnectBrowser } = tabsPkg; (async () => { - const browser = await chromium.connectOverCDP('http://127.0.0.1:9222'); + let browser; try { - const context = browser.contexts()[0]; - if (!context) { - throw new Error('No browser context available'); + const prepared = await preparePage(chromium); + browser = prepared.browser; + const page = prepared.page; + const targetUrl = 'https://me.meituan.com/ebooking/merchant/product#/index'; + const currentUrl = page.url(); + if (!currentUrl || !currentUrl.startsWith(targetUrl)) { + await page.goto(targetUrl); } - - await context.addInitScript(() => { - Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); - }); - - const tabIndex = Number(process.env.TAB_INDEX); - const page = await getOrCreateTab(context, tabIndex); - await page.goto('https://me.meituan.com/ebooking/merchant/product#/index'); } catch (error) { log.error(error); process.exitCode = 1; } finally { - if (browser) { - try { - if (typeof browser.disconnect === 'function') { - await browser.disconnect(); - } else { - await browser.close(); - } - } catch {} - } + await safeDisconnectBrowser(browser); } })(); diff --git a/src/main/utils/chrome/cdp.ts b/src/main/utils/chrome/cdp.ts new file mode 100644 index 0000000..4bd14d0 --- /dev/null +++ b/src/main/utils/chrome/cdp.ts @@ -0,0 +1,30 @@ +import type { Browser, BrowserContext } from 'playwright' + +export async function connectCdpContext(chromium: any, endpoint = 'http://127.0.0.1:9222'): Promise<{ browser: Browser, context: BrowserContext }> { + const browser = await chromium.connectOverCDP(endpoint) + const context = browser.contexts()[0] + + if (!context) { + throw new Error('No browser context available') + } + return { browser, context } +} + +export async function addStealthInit(context: BrowserContext) { + await context.addInitScript(() => { + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) + }) +} + +export async function safeDisconnectBrowser(browser: any) { + if (!browser) return + + try { + if (typeof browser.disconnect === 'function') { + await browser.disconnect() + } else { + await browser.close() + } + } catch {} +} + diff --git a/src/main/utils/chrome/launchLocalChrome.ts b/src/main/utils/chrome/launchLocalChrome.ts index 6ecf5d3..2ae8bbb 100644 --- a/src/main/utils/chrome/launchLocalChrome.ts +++ b/src/main/utils/chrome/launchLocalChrome.ts @@ -6,12 +6,12 @@ import { spawn } from 'child_process'; import log from 'electron-log'; // 启动本地浏览器 -export async function launchLocalChrome(options: any) { +export async function launchLocalChrome() { const chromePath = getChromePath(); // 多账号隔离 const profileDir = getProfileDir('default'); - log.info(`Launching Chrome with user data dir: ${options}`); + log.info(`Launching Chrome with user data dir: ${profileDir}`); // 检查端口是否被占用 const portInUse = await isPortInUse(9222); diff --git a/src/preload.ts b/src/preload.ts index 4478da5..2f2da9d 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -58,6 +58,9 @@ const api: WindowApi = { // 执行脚本 executeScript: (params: any) => ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params), + + // 打开渠道 + openChannel: (channels: any) => ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels), } contextBridge.exposeInMainWorld('api', api) \ No newline at end of file diff --git a/src/renderer/constant/channel.ts b/src/renderer/constant/channel.ts index 45e31d5..53ff4df 100644 --- a/src/renderer/constant/channel.ts +++ b/src/renderer/constant/channel.ts @@ -1,27 +1,25 @@ +import { v4 as uuidv4 } from 'uuid' + export interface Item { - id: number + id: string channelName: string + channelUrl: string } -export const channel: Item[] = [ +export const channels: Item[] = [ { - id: 1, - channelName: 'pms', + id: uuidv4(), + channelName: 'fliggy', + channelUrl: 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1', }, { - id: 2, - channelName: 'xc', + id: uuidv4(), + channelName: 'meituan', + channelUrl: 'https://me.meituan.com/ebooking/merchant/product#/index', }, { - id: 3, - channelName: 'fz', - }, - { - id: 4, - channelName: 'mt', - }, - { - id: 5, - channelName: 'dy', - }, + id: uuidv4(), + channelName: 'douyin', + channelUrl: 'https://life.douyin.com/p/goods_winetour/physical_room_list?groupid=1816249020842116', + } ] \ No newline at end of file diff --git a/src/renderer/constant/taskCenterList.ts b/src/renderer/constant/taskCenterList.ts index 09fdee8..6abb32a 100644 --- a/src/renderer/constant/taskCenterList.ts +++ b/src/renderer/constant/taskCenterList.ts @@ -5,7 +5,7 @@ export interface taskCenterItem { desc: string, id: string, icon: string, - type: 'sale' | 'close' | 'open' + type: 'sale' | 'close' | 'open' | 'channel' } export const taskCenterList: taskCenterItem[] = [ @@ -16,6 +16,13 @@ export const taskCenterList: taskCenterItem[] = [ icon: '销', type: 'sale' }, + { + title: '一键打开各渠道', + desc: '人工账号登录,为自动化操作做好准备', + id: uuidv4(), + icon: '渠', + type: 'channel' + }, { title: '关渠道房型', desc: '关闭销售渠道下的指定房型', diff --git a/src/renderer/views/home/TaskCenter.vue b/src/renderer/views/home/TaskCenter.vue index 0cb4628..3d999f7 100644 --- a/src/renderer/views/home/TaskCenter.vue +++ b/src/renderer/views/home/TaskCenter.vue @@ -33,6 +33,7 @@