import { useSyncExternalStore } from 'react'; import type { RoomTypeMapping } from '@api/types'; import type { SubTask, Task, TaskProgressPayload } from '@lib/task-types'; import { CONFIG_KEYS, IPC_EVENTS } from '../lib/constants'; import { invokeIpc, onIpc } from '../lib/host-api'; export interface TaskStoreState { initialized: boolean; tasks: Task[]; } type RoomTypeMappingLike = RoomTypeMapping & { dyHotSpringName?: string; [key: string]: any; }; export type TaskOperationInput = { taskId?: string; roomType: string; startTime: string; endTime: string; operation: 'open' | 'close'; roomList: RoomTypeMappingLike[]; }; type ExecuteTaskResult = { success: boolean; error?: string; result?: unknown; }; const listeners = new Set<() => void>(); let eventSubscriptionsBound = false; let initPromise: Promise | null = null; let state: TaskStoreState = { initialized: false, tasks: [], }; function emit(): void { for (const listener of listeners) { listener(); } } function patchState(patch: Partial): TaskStoreState { state = { ...state, ...patch }; emit(); return state; } function deriveTaskStatus(task: Task): Task['status'] { if (task.subTasks.every((subTask) => subTask.status === 'success')) return 'success'; if (task.subTasks.every((subTask) => subTask.status === 'failed')) return 'failed'; if (task.subTasks.some((subTask) => subTask.status === 'failed') && task.subTasks.some((subTask) => subTask.status === 'success')) { return 'partial_failed'; } if (task.subTasks.some((subTask) => subTask.status === 'running')) return 'running'; return 'pending'; } function normalizeRoomType(item: RoomTypeMappingLike): RoomTypeMappingLike { return { ...item, dyHotSpringName: item.dyHotSpringName ?? item.dyHotSrpingName ?? '', dyHotSrpingName: item.dyHotSrpingName ?? item.dyHotSpringName ?? '', }; } function normalizeRoomList(roomList: RoomTypeMappingLike[]): RoomTypeMappingLike[] { return Array.isArray(roomList) ? roomList.map((item) => normalizeRoomType(item ?? {})) : []; } function buildTaskSubTasks(taskId: string, roomType: RoomTypeMappingLike | undefined): SubTask[] { const scriptMappings = [ { prop: 'fzName', scriptId: 'fg_trace.js', name: '飞猪房态追踪', hasValue: Boolean(roomType?.fzName) }, { prop: 'mtName', scriptId: 'mt_trace.js', name: '美团房态追踪', hasValue: Boolean(roomType?.mtName) }, { prop: 'dyHotelName', scriptId: 'dy_hotel_trace.js', name: '抖音酒店房态追踪', hasValue: Boolean(roomType?.dyHotelName) }, { prop: 'dyHotSpringName', scriptId: 'dy_hot_spring_trace.js', name: '抖音温泉房态追踪', hasValue: Boolean(roomType?.dyHotSpringName || roomType?.dyHotSrpingName), }, ]; return scriptMappings .filter((mapping) => mapping.hasValue) .map((mapping) => ({ id: `${taskId}_${mapping.prop}`, taskId, scriptId: mapping.scriptId, name: mapping.name, status: 'pending' as const, progress: 0, message: '等待执行', stdoutTail: '', stderrTail: '', startedAt: new Date().toISOString(), })); } async function persistTasks(tasks: Task[]): Promise { try { await invokeIpc(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.TASK_LIST, tasks); } catch { // ignore persistence failures in UI store } } function updateTaskCollection(updater: (tasks: Task[]) => Task[]): void { const nextTasks = updater(state.tasks); patchState({ tasks: nextTasks }); void persistTasks(nextTasks); } function markTaskFailed(taskId: string, error: string): void { updateTaskCollection((tasks) => tasks.map((task) => { if (task.id !== taskId) return task; const nextSubTasks = task.subTasks.map((subTask) => { if (subTask.status === 'success') return subTask; return { ...subTask, status: 'failed' as const, message: error, error, completedAt: new Date().toISOString(), }; }); return { ...task, subTasks: nextSubTasks, status: deriveTaskStatus({ ...task, subTasks: nextSubTasks }), updatedAt: new Date().toISOString(), }; })); } function handleTaskProgress(payload: TaskProgressPayload & { taskId: string; subTaskId: string }): void { updateTaskCollection((tasks) => tasks.map((task) => { if (task.id !== payload.taskId) return task; const nextSubTasks: SubTask[] = task.subTasks.map((subTask) => { if (subTask.id !== payload.subTaskId) return subTask; const nextStatus: SubTask['status'] = subTask.status === 'pending' ? 'running' : subTask.status; return { ...subTask, status: nextStatus, progress: payload.progress ?? subTask.progress, message: payload.message ?? subTask.message, stdoutTail: payload.stdoutTail ?? subTask.stdoutTail, stderrTail: payload.stderrTail ?? subTask.stderrTail, }; }); return { ...task, subTasks: nextSubTasks, status: deriveTaskStatus({ ...task, subTasks: nextSubTasks }), updatedAt: new Date().toISOString(), }; })); } function handleTaskCompleted(payload: { taskId: string; subTaskId: string; success: boolean; exitCode: number | null; error?: string }): void { updateTaskCollection((tasks) => tasks.map((task) => { if (task.id !== payload.taskId) return task; const nextSubTasks: SubTask[] = task.subTasks.map((subTask) => { if (subTask.id !== payload.subTaskId) return subTask; const nextStatus: SubTask['status'] = payload.success ? 'success' : 'failed'; return { ...subTask, status: nextStatus, progress: payload.success ? 100 : subTask.progress, error: payload.error, completedAt: new Date().toISOString(), }; }); return { ...task, subTasks: nextSubTasks, status: deriveTaskStatus({ ...task, subTasks: nextSubTasks }), updatedAt: new Date().toISOString(), }; })); } async function loadTasks(): Promise { try { const savedTasks = await invokeIpc(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.TASK_LIST); patchState({ initialized: true, tasks: Array.isArray(savedTasks) ? savedTasks : [], }); } catch { patchState({ initialized: true, tasks: [], }); } } async function initTaskStore(): Promise { if (!initPromise) { initPromise = loadTasks(); } if (!eventSubscriptionsBound) { eventSubscriptionsBound = true; onIpc(IPC_EVENTS.TASK_PROGRESS, handleTaskProgress as (...args: any[]) => void); onIpc(IPC_EVENTS.TASK_STARTED, ((payload: { taskId: string; subTaskId: string }) => { handleTaskProgress({ ...payload, progress: 0, message: '开始执行', }); }) as (...args: any[]) => void); onIpc(IPC_EVENTS.TASK_COMPLETED, handleTaskCompleted as (...args: any[]) => void); } await initPromise; } function createTask(options: TaskOperationInput): Task { const taskId = options.taskId ?? crypto.randomUUID(); const roomList = normalizeRoomList(options.roomList); const roomType = roomList.find((item) => item.id === options.roomType); const subTasks = buildTaskSubTasks(taskId, roomType); const task: Task = { id: taskId, title: `${options.operation === 'open' ? '开启' : '关闭'}渠道房型 - ${roomType?.pmsName || ''}`, operation: options.operation, roomType: options.roomType, dateRange: [options.startTime, options.endTime], status: 'pending', subTasks, roomList, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; updateTaskCollection((tasks) => [task, ...tasks]); return task; } async function executeTask(taskId: string): Promise { const task = state.tasks.find((item) => item.id === taskId); if (!task) { return { success: false, error: '任务不存在,无法执行。' }; } try { const result = await invokeIpc(IPC_EVENTS.EXECUTE_SCRIPT, { taskId: task.id, roomType: task.roomType, startTime: task.dateRange[0], endTime: task.dateRange[1], operation: task.operation, roomList: normalizeRoomList(task.roomList as RoomTypeMappingLike[]), }); if (!result?.success) { markTaskFailed(task.id, result?.error || '任务执行失败'); } return result; } catch (error) { const message = error instanceof Error ? error.message : String(error); markTaskFailed(task.id, message); return { success: false, error: message, }; } } async function createAndExecuteTask(options: TaskOperationInput): Promise<{ task: Task; result: ExecuteTaskResult }> { const task = createTask(options); const result = await executeTask(task.id); return { task, result }; } async function retryFailedSubTasks(taskId: string): Promise { const task = state.tasks.find((item) => item.id === taskId); if (!task) { return { success: false, error: '任务不存在,无法重试。' }; } const hasFailedSubTask = task.subTasks.some((subTask) => subTask.status === 'failed'); if (!hasFailedSubTask) { return { success: false, error: '当前任务没有可重试的失败子任务。' }; } updateTaskCollection((tasks) => tasks.map((currentTask) => { if (currentTask.id !== taskId) return currentTask; const nextSubTasks = currentTask.subTasks.map((subTask) => { if (subTask.status !== 'failed') return subTask; return { ...subTask, status: 'pending' as const, progress: 0, message: '等待执行', stdoutTail: '', stderrTail: '', error: undefined, completedAt: undefined, }; }); return { ...currentTask, subTasks: nextSubTasks, status: 'pending', updatedAt: new Date().toISOString(), }; })); return executeTask(taskId); } function removeTask(taskId: string): void { updateTaskCollection((tasks) => tasks.filter((task) => task.id !== taskId)); } function subscribe(listener: () => void): () => void { listeners.add(listener); return () => listeners.delete(listener); } function getSnapshot(): TaskStoreState { return state; } export const taskStore = { subscribe, getSnapshot, getState: () => state, init: initTaskStore, load: loadTasks, createTask, createAndExecuteTask, retryFailedSubTasks, executeTask, removeTask, }; export function useTaskStore(): TaskStoreState { return useSyncExternalStore(taskStore.subscribe, taskStore.getSnapshot, taskStore.getSnapshot); } export function getPendingTasks(tasks = state.tasks): Task[] { return tasks.filter((task) => task.status === 'pending' || task.status === 'running'); } export function getCompletedTasks(tasks = state.tasks): Task[] { return tasks.filter((task) => task.status === 'success' || task.status === 'failed' || task.status === 'partial_failed'); }