Files
zn-ai/src/stores/task.ts
duanshuwen dfa4388087 refactor: optimize component rendering with memoization and improve state management
- Added memoization to ChatHistoryPanel, ChatMessageList, and TaskBoard components to prevent unnecessary re-renders.
- Refactored HomePage to utilize useMemo for derived state calculations, enhancing performance.
- Updated main.tsx to conditionally render React.StrictMode based on the environment.
- Improved chat and channel store hooks to allow for selector functions, enhancing flexibility in state selection.
- Enhanced streaming message handling in chat store to manage pending deltas more effectively.
- Refactored LoginPage to include animated decorations for improved user experience.
- Implemented lazy loading for routes in the router to optimize initial load time.
2026-04-18 11:05:49 +08:00

369 lines
11 KiB
TypeScript

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<void> | null = null;
let state: TaskStoreState = {
initialized: false,
tasks: [],
};
function emit(): void {
for (const listener of listeners) {
listener();
}
}
function patchState(patch: Partial<TaskStoreState>): 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<void> {
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<void> {
try {
const savedTasks = await invokeIpc<Task[]>(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<void> {
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<ExecuteTaskResult> {
const task = state.tasks.find((item) => item.id === taskId);
if (!task) {
return { success: false, error: '任务不存在,无法执行。' };
}
try {
const result = await invokeIpc<ExecuteTaskResult>(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<ExecuteTaskResult> {
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<T = TaskStoreState>(selector?: (state: TaskStoreState) => T): T {
const select = selector ?? ((current: TaskStoreState) => current as unknown as T);
return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(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');
}