feat: add task management and progress reporting
- 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.
This commit is contained in:
@@ -55,6 +55,23 @@ const api: WindowApi = {
|
||||
// 执行脚本
|
||||
executeScript: (params: any) => ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params),
|
||||
|
||||
// 任务事件
|
||||
onTaskProgress: (cb: (payload: any) => void) => {
|
||||
const subscription = (_event: any, payload: any) => cb(payload);
|
||||
ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, subscription);
|
||||
return () => ipcRenderer.removeListener(IPC_EVENTS.TASK_PROGRESS, subscription);
|
||||
},
|
||||
onTaskStarted: (cb: (payload: any) => void) => {
|
||||
const subscription = (_event: any, payload: any) => cb(payload);
|
||||
ipcRenderer.on(IPC_EVENTS.TASK_STARTED, subscription);
|
||||
return () => ipcRenderer.removeListener(IPC_EVENTS.TASK_STARTED, subscription);
|
||||
},
|
||||
onTaskCompleted: (cb: (payload: any) => void) => {
|
||||
const subscription = (_event: any, payload: any) => cb(payload);
|
||||
ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, subscription);
|
||||
return () => ipcRenderer.removeListener(IPC_EVENTS.TASK_COMPLETED, subscription);
|
||||
},
|
||||
|
||||
// 打开渠道
|
||||
openChannel: (channels: any) => ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { ipcMain, app } from 'electron';
|
||||
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';
|
||||
@@ -15,6 +15,7 @@ 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>()
|
||||
|
||||
@@ -228,7 +229,7 @@ export function runTaskOperationService() {
|
||||
// 执行脚本
|
||||
ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (_event, options: any) => {
|
||||
try {
|
||||
// 从options.roomList列表中找到对应的名称
|
||||
const taskId = options.taskId || randomUUID();
|
||||
const roomType = options.roomList.find((item: any) => item.id === options.roomType);
|
||||
|
||||
const pairs: Array<[string, string]> = [
|
||||
@@ -246,33 +247,82 @@ export function runTaskOperationService() {
|
||||
if (!fs.existsSync(p)) {
|
||||
throw new Error(`Script not found for channel ${channel}: ${p}`)
|
||||
}
|
||||
return { channel, scriptPath: 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 channelNameMap: Record<string, string> = {
|
||||
fzName: 'fliggy',
|
||||
mtName: 'meituan',
|
||||
dyHotelName: 'douyin',
|
||||
dyHotSpringName: 'douyin',
|
||||
}
|
||||
const defaultTabIndexMap: Record<string, number> = {
|
||||
fliggy: 0,
|
||||
meituan: 1,
|
||||
douyin: 2
|
||||
}
|
||||
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,
|
||||
})
|
||||
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,
|
||||
|
||||
@@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js';
|
||||
|
||||
const { preparePage, safeDisconnectBrowser } = tabsPkg;
|
||||
|
||||
function reportProgress(step, percent) {
|
||||
console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent }));
|
||||
}
|
||||
|
||||
const parseDateInput = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr);
|
||||
@@ -158,6 +162,7 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
let browser;
|
||||
|
||||
try {
|
||||
reportProgress('连接本地浏览器', 10);
|
||||
const groupId = '1816249020842116';
|
||||
const homeUrl = `https://life.douyin.com/p/home?groupid=${groupId}`;
|
||||
const priceAmountStateUrl = `https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=${groupId}`;
|
||||
@@ -175,6 +180,7 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
reportProgress('定位目标页面', 30);
|
||||
// Navigation logic (User provided)
|
||||
try {
|
||||
// Try to click the store/login button if visible
|
||||
@@ -199,12 +205,14 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
|
||||
if (!roomType || !startDate) {
|
||||
log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.');
|
||||
reportProgress('执行完成', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for table rows to appear
|
||||
await page.locator('.lifep-table-row').first().waitFor({ state: 'visible', timeout: 15000 });
|
||||
|
||||
reportProgress('操作房态数据', 60);
|
||||
const dateList = buildDateList(startDate, endDate);
|
||||
for (const dateStr of dateList) {
|
||||
try {
|
||||
@@ -215,11 +223,13 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
await page.waitForTimeout(500 + Math.random() * 500);
|
||||
}
|
||||
|
||||
reportProgress('保存并校验', 90);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await safeDisconnectBrowser(browser);
|
||||
reportProgress('执行完成', 100);
|
||||
process.exit(process.exitCode || 0);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js';
|
||||
|
||||
const { preparePage, safeDisconnectBrowser } = tabsPkg;
|
||||
|
||||
function reportProgress(step, percent) {
|
||||
console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent }));
|
||||
}
|
||||
|
||||
const parseDateInput = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr);
|
||||
@@ -158,6 +162,7 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
let browser;
|
||||
|
||||
try {
|
||||
reportProgress('连接本地浏览器', 10);
|
||||
const groupId = '1816249020842116';
|
||||
const homeUrl = `https://life.douyin.com/p/home?groupid=${groupId}`;
|
||||
const priceAmountStateUrl = `https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=${groupId}`;
|
||||
@@ -175,6 +180,7 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
reportProgress('定位目标页面', 30);
|
||||
// Navigation logic (User provided)
|
||||
try {
|
||||
// Try to click the store/login button if visible
|
||||
@@ -199,12 +205,14 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
|
||||
if (!roomType || !startDate) {
|
||||
log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.');
|
||||
reportProgress('执行完成', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for table rows to appear
|
||||
await page.locator('.lifep-table-row').first().waitFor({ state: 'visible', timeout: 15000 });
|
||||
|
||||
reportProgress('操作房态数据', 60);
|
||||
const dateList = buildDateList(startDate, endDate);
|
||||
for (const dateStr of dateList) {
|
||||
try {
|
||||
@@ -215,11 +223,13 @@ const navigateToRoomStatusManagement = async (page) => {
|
||||
await page.waitForTimeout(500 + Math.random() * 500);
|
||||
}
|
||||
|
||||
reportProgress('保存并校验', 90);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await safeDisconnectBrowser(browser);
|
||||
reportProgress('执行完成', 100);
|
||||
process.exit(process.exitCode || 0);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js';
|
||||
|
||||
const { preparePage, safeDisconnectBrowser } = tabsPkg;
|
||||
|
||||
function reportProgress(step, percent) {
|
||||
console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent }));
|
||||
}
|
||||
|
||||
const parseDateInput = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr);
|
||||
@@ -137,6 +141,7 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation
|
||||
let browser;
|
||||
|
||||
try {
|
||||
reportProgress('连接本地浏览器', 10);
|
||||
const targetUrl = 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1';
|
||||
const prepared = await preparePage(chromium, { targetUrl });
|
||||
browser = prepared.browser;
|
||||
@@ -144,6 +149,7 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation
|
||||
|
||||
await page.waitForTimeout(4000 + Math.random() * 300);
|
||||
|
||||
reportProgress('定位目标页面', 30);
|
||||
await ensureRoomPriceCalendar(page);
|
||||
|
||||
const roomType = process.env.ROOM_TYPE;
|
||||
@@ -153,12 +159,14 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation
|
||||
|
||||
if (!roomType || !startDate) {
|
||||
log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.');
|
||||
reportProgress('执行完成', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = page.locator('#price-reserve-table-container');
|
||||
await container.waitFor({ state: 'visible' });
|
||||
|
||||
reportProgress('操作房态数据', 60);
|
||||
const dateList = buildDateList(startDate, endDate);
|
||||
for (const mmdd of dateList) {
|
||||
const dateIndex = await findHeaderDateIndex(container, mmdd);
|
||||
@@ -167,12 +175,15 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation
|
||||
}
|
||||
await toggleRoomByDateIndex(container, { roomType, dateIndex, operation });
|
||||
await page.waitForTimeout(600 + Math.random() * 600);
|
||||
}
|
||||
}
|
||||
|
||||
reportProgress('保存并校验', 90);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await safeDisconnectBrowser(browser);
|
||||
reportProgress('执行完成', 100);
|
||||
process.exit(process.exitCode || 0);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js';
|
||||
|
||||
const { preparePage, safeDisconnectBrowser } = tabsPkg;
|
||||
|
||||
function reportProgress(step, percent) {
|
||||
console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent }));
|
||||
}
|
||||
|
||||
const parseDateInput = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr);
|
||||
@@ -173,6 +177,7 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => {
|
||||
let browser;
|
||||
|
||||
try {
|
||||
reportProgress('连接本地浏览器', 10);
|
||||
const targetUrl = 'https://me.meituan.com/ebooking/merchant/product#/index';
|
||||
const prepared = await preparePage(chromium, { targetUrl });
|
||||
browser = prepared.browser;
|
||||
@@ -180,6 +185,7 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => {
|
||||
|
||||
await page.waitForTimeout(4000 + Math.random() * 300);
|
||||
|
||||
reportProgress('定位目标页面', 30);
|
||||
// Navigation logic from user snippet
|
||||
try {
|
||||
const menu = page.locator('#vina-menu-item-100114001 > .lz-submenu-title');
|
||||
@@ -194,7 +200,7 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => {
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const calendarLink = page.locator('a').filter({ hasText: '房价房量日历' });
|
||||
if (await calendarLink.isVisible()) {
|
||||
await calendarLink.click();
|
||||
@@ -211,12 +217,14 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => {
|
||||
|
||||
if (!roomType || !startDate) {
|
||||
log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.');
|
||||
reportProgress('执行完成', 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for table
|
||||
await page.locator('.vxe-table--body-wrapper').waitFor({ state: 'visible', timeout: 15000 });
|
||||
|
||||
reportProgress('操作房态数据', 60);
|
||||
const dateList = buildDateList(startDate, endDate);
|
||||
for (const mmdd of dateList) {
|
||||
try {
|
||||
@@ -227,11 +235,13 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => {
|
||||
await page.waitForTimeout(600 + Math.random() * 600);
|
||||
}
|
||||
|
||||
reportProgress('保存并校验', 90);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await safeDisconnectBrowser(browser);
|
||||
reportProgress('执行完成', 100);
|
||||
process.exit(process.exitCode || 0);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -15,6 +15,7 @@ const DEFAULT_CONFIG: IConfig = {
|
||||
[CONFIG_KEYS.DEFAULT_MODEL]: null,
|
||||
[CONFIG_KEYS.SELECTED_CHANNELS]: [],
|
||||
[CONFIG_KEYS.IMAGE_CACHE]: [],
|
||||
[CONFIG_KEYS.TASK_LIST]: [],
|
||||
}
|
||||
|
||||
export class ConfigService {
|
||||
|
||||
@@ -8,6 +8,8 @@ export class executeScriptService extends EventEmitter {
|
||||
async executeScript(
|
||||
scriptPath: string,
|
||||
options: Record<string, any>,
|
||||
taskId?: string,
|
||||
subTaskId?: string,
|
||||
): Promise<{ success: boolean; exitCode: number | null; stdoutTail: string; stderrTail: string; error?: string }> {
|
||||
const MAX_TAIL = 32 * 1024;
|
||||
|
||||
@@ -48,6 +50,20 @@ export class executeScriptService extends EventEmitter {
|
||||
const text = data.toString();
|
||||
stdoutTail = appendTail(stdoutTail, text);
|
||||
log.info(`stdout: ${text}`);
|
||||
|
||||
if (text.includes('__ZN_PROGRESS__')) {
|
||||
try {
|
||||
const jsonStr = text.split('__ZN_PROGRESS__')[1]?.trim();
|
||||
if (jsonStr) {
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
this.emit('progress', { taskId, subTaskId, ...parsed });
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('stdout', { taskId, subTaskId, text });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +72,7 @@ export class executeScriptService extends EventEmitter {
|
||||
const text = data.toString();
|
||||
stderrTail = appendTail(stderrTail, text);
|
||||
log.info(`stderr: ${text}`);
|
||||
this.emit('stderr', { taskId, subTaskId, text });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user