import { ipcMain, app, BrowserWindow } from 'electron'; import { IPC_EVENTS } from '@lib/constants'; import { launchLocalChrome } from '@electron/utils/chrome/launchLocalChrome' import { executeScriptService } from '@electron/service/execute-script-service'; import { listScripts, getScript, saveScript, deleteScript, toggleScript, } from '@electron/service/script-store-service'; import { runScriptById } from '@electron/service/script-execution-service'; import fs from 'fs' import path from 'path' import { spawn } from 'child_process' import log from 'electron-log'; import { randomUUID } from 'crypto'; const openedTabIndexByChannelName = new Map() function getScriptsDir() { return app.isPackaged ? path.join(__dirname, 'scripts') : path.join(process.cwd(), 'electron/scripts') } export function runTaskOperationService() { const executeScriptServiceInstance = new executeScriptService(); const playwrightCoreDir = path.dirname(require.resolve('playwright-core')); const cliPath = path.join(playwrightCoreDir, 'cli.js'); let recorderProc: ReturnType | null = null; // 脚本管理 IPC ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => { try { return listScripts(); } catch (error: any) { log.error('[SCRIPT_LIST] error:', error); throw error; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_GET, async (_event, id: string) => { try { return getScript(id); } catch (error: any) { log.error('[SCRIPT_GET] error:', error); throw error; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_SAVE, async (_event, input: any) => { try { return saveScript(input); } catch (error: any) { log.error('[SCRIPT_SAVE] error:', error); throw error; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_DELETE, async (_event, id: string) => { try { return deleteScript(id); } catch (error: any) { log.error('[SCRIPT_DELETE] error:', error); throw error; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_TOGGLE, async (_event, id: string, enabled: boolean) => { try { return toggleScript(id, enabled); } catch (error: any) { log.error('[SCRIPT_TOGGLE] error:', error); throw error; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_RUN, async (_event, id: string) => { try { const script = getScript(id); return await runScriptById(id, script?.channel); } catch (error: any) { log.error('[SCRIPT_RUN] error:', error); return { success: false, exitCode: null, stdoutTail: '', stderrTail: '', error: error?.message || 'Run failed' }; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url?: string) => { try { if (recorderProc) { recorderProc.kill('SIGINT'); recorderProc = null; } const targetUrl = url || 'about:blank'; recorderProc = spawn(process.execPath, [cliPath, 'codegen', '--target', 'javascript', '--channel', 'chrome', '--viewport-size', '1920,1080', '--color-scheme', 'light', targetUrl], { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, stdio: 'pipe', }); recorderProc.on('error', (err) => { log.error('[SCRIPT_RECORD_START] Failed to start codegen process:', err); }); recorderProc.on('exit', (code, signal) => { log.info(`[SCRIPT_RECORD_START] Process exited code=${code} signal=${signal}`); recorderProc = null; }); recorderProc.stdout?.on('data', (data: Buffer) => { log.info(`[SCRIPT_RECORD_START] stdout: ${data.toString()}`); }); recorderProc.stderr?.on('data', (data: Buffer) => { log.error(`[SCRIPT_RECORD_START] stderr: ${data.toString()}`); }); return { success: true }; } catch (error: any) { log.error('[SCRIPT_RECORD_START] error:', error); return { success: false, error: error?.message || 'Recording start failed' }; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => { try { if (recorderProc) { recorderProc.kill('SIGINT'); recorderProc = null; } return { success: true, code: '' }; } catch (error: any) { log.error('[SCRIPT_RECORD_STOP] error:', error); return { success: false, error: error?.message || 'Recording stop failed' }; } }); ipcMain.handle(IPC_EVENTS.SCRIPT_CODEGEN, async (_event, id: string, url?: string) => { try { const script = getScript(id); if (!script) { return { success: false, error: 'Script not found' }; } const scriptsDir = getScriptsDir(); const scriptPath = path.join(scriptsDir, script.filename); const targetUrl = url || 'about:blank'; log.info(`[SCRIPT_CODEGEN] Starting codegen for script ${id} at ${scriptPath} with url ${targetUrl}`); return await new Promise<{ success: boolean; code?: string; error?: string }>((resolve) => { const proc = spawn(process.execPath, [cliPath, 'codegen', '--target', 'javascript', '--channel', 'chrome', '-o', scriptPath, targetUrl], { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, stdio: 'pipe', }); proc.on('exit', () => { try { let generatedCode = fs.readFileSync(scriptPath, 'utf-8'); // Playwright codegen --target javascript generates CommonJS code. // Since script files use .mjs extension, we inject createRequire for compatibility. if (generatedCode.includes("require('playwright')") && !generatedCode.includes('createRequire')) { generatedCode = `import { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\n\n${generatedCode}`; } fs.writeFileSync(scriptPath, generatedCode, 'utf-8'); // Update script store so the new code is reflected in metadata reads saveScript({ id, name: script.name, description: script.description, code: generatedCode, channel: script.channel, enabled: script.enabled, }); resolve({ success: true, code: generatedCode }); } catch (err: any) { log.error('[SCRIPT_CODEGEN] Failed to process generated code:', err); resolve({ success: false, error: err?.message || 'Failed to process generated code' }); } }); proc.on('error', (err) => { log.error('[SCRIPT_CODEGEN] Failed to start codegen:', err); resolve({ success: false, error: err.message }); }); }); } catch (error: any) { log.error('[SCRIPT_CODEGEN] error:', error); return { success: false, error: error?.message || 'Codegen failed' }; } }); // 打开渠道 ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels: any) => { try { await launchLocalChrome() const scriptsDir = getScriptsDir() const scriptPath = path.join(scriptsDir, 'open_all_channel.js') openedTabIndexByChannelName.clear() const validChannels = Array.isArray(channels) ? channels.filter((c) => c && typeof c === 'object' && c.channelUrl) : [] if (validChannels.length === 0) { return { success: false, error: '没有可用的渠道配置' } } for (let i = 0; i < validChannels.length; i++) { const name = validChannels[i]?.channelName if (name) openedTabIndexByChannelName.set(String(name), i) } const result = await executeScriptServiceInstance.executeScript(scriptPath, { channels: validChannels }) return { success: true, result } } catch (error) { return { success: false, error: (error as any)?.message || 'open channel failed' } } }) // 执行脚本 ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (_event, options: any) => { try { const taskId = options.taskId || randomUUID(); const roomTypeRaw = options.roomList.find((item: any) => item.id === options.roomType); const roomType = roomTypeRaw ? { ...roomTypeRaw, dyHotSpringName: roomTypeRaw.dyHotSpringName ?? roomTypeRaw.dyHotSrpingName, dyHotSrpingName: roomTypeRaw.dyHotSrpingName ?? roomTypeRaw.dyHotSpringName, } : null; const pairs: Array<[string, string]> = [ ['fzName', 'fg_trace.js'], ['mtName', 'mt_trace.js'], ['dyHotelName', 'dy_hotel_trace.js'], ['dyHotSpringName', 'dy_hot_spring_trace.js'] ] const scriptEntries = pairs.filter(([prop]) => { if (prop === 'dyHotSpringName') { return roomType?.dyHotSpringName || roomType?.dyHotSrpingName } return roomType?.[prop] }) const scriptsDir = getScriptsDir() const scriptPaths = scriptEntries.map(([channel, fileName]) => { const p = path.join(scriptsDir, fileName) if (!fs.existsSync(p)) { throw new Error(`Script not found for channel ${channel}: ${p}`) } return { channel, fileName, scriptPath: p } }) const channelNameMap: Record = { fzName: 'fliggy', mtName: 'meituan', dyHotelName: 'douyin', dyHotSpringName: 'douyin', } const defaultTabIndexMap: Record = { fliggy: 0, meituan: 1, douyin: 2 } const results: any[] = [] for (let i = 0; i < scriptPaths.length; i++) { const item = scriptPaths[i] const subTaskId = `${taskId}_${item.channel}`; const mappedName = channelNameMap[item.channel] const tabIndex = mappedName ? (openedTabIndexByChannelName.get(mappedName) ?? defaultTabIndexMap[mappedName] ?? i) : i const win = BrowserWindow.getAllWindows()[0]; win?.webContents.send(IPC_EVENTS.TASK_STARTED, { taskId, subTaskId, scriptId: item.fileName, name: item.channel, }); const onProgress = (payload: any) => { win?.webContents.send(IPC_EVENTS.TASK_PROGRESS, payload); }; const onStdout = (payload: any) => { win?.webContents.send(IPC_EVENTS.TASK_PROGRESS, { ...payload, stdoutTail: payload.text, }); }; const onStderr = (payload: any) => { win?.webContents.send(IPC_EVENTS.TASK_PROGRESS, { ...payload, stderrTail: payload.text, }); }; executeScriptServiceInstance.on('progress', onProgress); executeScriptServiceInstance.on('stdout', onStdout); executeScriptServiceInstance.on('stderr', onStderr); log.info(`Launching script for channel ${item.channel}: ${item.scriptPath} (tabIndex: ${tabIndex})`) const result = await executeScriptServiceInstance.executeScript( item.scriptPath, { roomType: item.channel === 'dyHotSpringName' ? (roomType.dyHotSpringName || roomType.dyHotSrpingName) : roomType[item.channel], startTime: options.startTime, endTime: options.endTime, operation: options.operation, tabIndex, }, taskId, subTaskId, ); executeScriptServiceInstance.off('progress', onProgress); executeScriptServiceInstance.off('stdout', onStdout); executeScriptServiceInstance.off('stderr', onStderr); win?.webContents.send(IPC_EVENTS.TASK_COMPLETED, { taskId, subTaskId, success: result.success, exitCode: result.exitCode, error: result.error, }); results.push({ channel: item.channel, scriptPath: item.scriptPath, ...result, }) } return { success: true, result: results }; } catch (error: any) { return { success: false, error: error.message }; } }); }