- Implemented task and subtask structures with progress tracking. - Added reporting functionality to log progress at various stages in hotel room status management scripts. - Created a task store to manage tasks and their states, including persistence to local storage. - Updated UI components to display task lists and handle task actions (retry, remove). - Removed deprecated TaskCard and TaskList components, replacing them with a new structure for better maintainability. - Enhanced script execution service to emit progress events for UI updates.
339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
|
|
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<string, number>()
|
|
|
|
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<typeof spawn> | 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 roomType = options.roomList.find((item: any) => item.id === options.roomType);
|
|
|
|
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]) => 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<string, string> = {
|
|
fzName: 'fliggy',
|
|
mtName: 'meituan',
|
|
dyHotelName: 'douyin',
|
|
dyHotSpringName: 'douyin',
|
|
}
|
|
const defaultTabIndexMap: Record<string, number> = {
|
|
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: 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 };
|
|
}
|
|
});
|
|
}
|