Refactor UUID generation, remove unused logger and encryption utilities, and clean up request handling
- Updated `generateUUID` function for improved readability and performance. - Deleted `logger.ts`, `other.ts`, `request.ts`, `storage.ts`, `tansParams.ts`, and `validate.ts` as they were no longer needed. - Simplified TypeScript configuration by removing unnecessary paths and aliases. - Enhanced Vite configuration for better project structure and maintainability.
This commit is contained in:
@@ -1,165 +1,367 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { IPC_EVENTS, CONFIG_KEYS } from '@lib/constants';
|
||||
import type { Task, SubTask, TaskProgressPayload } from '@lib/task-types';
|
||||
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 const useTaskStore = defineStore('task', () => {
|
||||
const tasks = ref<Task[]>([]);
|
||||
let initialized = false;
|
||||
export interface TaskStoreState {
|
||||
initialized: boolean;
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
// 初始化时从 electron-store 加载
|
||||
const init = async () => {
|
||||
if (initialized) return;
|
||||
try {
|
||||
const saved = await window.api.invoke(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.TASK_LIST);
|
||||
if (Array.isArray(saved)) {
|
||||
tasks.value = saved;
|
||||
}
|
||||
initialized = true;
|
||||
} catch (e) {
|
||||
console.error('Failed to load tasks from store', e);
|
||||
}
|
||||
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 ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
// 持久化 helper(用于减少写盘次数,可在需要时调用)
|
||||
const persist = async () => {
|
||||
try {
|
||||
const payload = JSON.parse(JSON.stringify(tasks.value))
|
||||
await window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.TASK_LIST, payload);
|
||||
} catch (e) {
|
||||
console.error('Failed to persist tasks', e);
|
||||
}
|
||||
};
|
||||
function normalizeRoomList(roomList: RoomTypeMappingLike[]): RoomTypeMappingLike[] {
|
||||
return Array.isArray(roomList) ? roomList.map((item) => normalizeRoomType(item ?? {})) : [];
|
||||
}
|
||||
|
||||
const createTask = (options: any): Task => {
|
||||
const taskId = crypto.randomUUID();
|
||||
const roomTypeObj = options.roomList.find((item: any) => item.id === options.roomType);
|
||||
const scriptMappings = [
|
||||
{ prop: 'fzName', scriptId: 'fg_trace.js', name: '飞猪房态追踪' },
|
||||
{ prop: 'mtName', scriptId: 'mt_trace.js', name: '美团房态追踪' },
|
||||
{ prop: 'dyHotelName', scriptId: 'dy_hotel_trace.js', name: '抖音酒店房态追踪' },
|
||||
{ prop: 'dyHotSpringName', scriptId: 'dy_hot_spring_trace.js', name: '抖音温泉房态追踪' },
|
||||
];
|
||||
const subTasks: SubTask[] = scriptMappings
|
||||
.filter(({ prop }) => roomTypeObj?.[prop])
|
||||
.map(({ scriptId, name, prop }) => ({
|
||||
id: `${taskId}_${prop}`,
|
||||
taskId,
|
||||
scriptId,
|
||||
name,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
message: '等待执行',
|
||||
stdoutTail: '',
|
||||
stderrTail: '',
|
||||
startedAt: new Date().toISOString(),
|
||||
}));
|
||||
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),
|
||||
},
|
||||
];
|
||||
|
||||
const task: Task = {
|
||||
id: taskId,
|
||||
title: `${options.operation === 'open' ? '开启' : '关闭'}渠道房型 - ${roomTypeObj?.pmsName || ''}`,
|
||||
operation: options.operation,
|
||||
roomType: options.roomType,
|
||||
dateRange: [options.startTime, options.endTime],
|
||||
status: 'pending',
|
||||
subTasks,
|
||||
roomList: options.roomList || [],
|
||||
createdAt: new Date().toISOString(),
|
||||
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(),
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
tasks.value = [task, ...tasks.value];
|
||||
persist();
|
||||
return task;
|
||||
};
|
||||
function handleTaskProgress(payload: TaskProgressPayload & { taskId: string; subTaskId: string }): void {
|
||||
updateTaskCollection((tasks) => tasks.map((task) => {
|
||||
if (task.id !== payload.taskId) return task;
|
||||
|
||||
const updateSubTaskProgress = (payload: TaskProgressPayload & { taskId: string; subTaskId: string }) => {
|
||||
const task = tasks.value.find((t) => t.id === payload.taskId);
|
||||
if (!task) return;
|
||||
const subTask = task.subTasks.find((s) => s.id === payload.subTaskId);
|
||||
if (!subTask) return;
|
||||
if (subTask.status === 'pending') subTask.status = 'running';
|
||||
if (payload.progress !== undefined) subTask.progress = payload.progress;
|
||||
if (payload.message !== undefined) subTask.message = payload.message;
|
||||
if (payload.stdoutTail !== undefined) subTask.stdoutTail = payload.stdoutTail;
|
||||
if (payload.stderrTail !== undefined) subTask.stderrTail = payload.stderrTail;
|
||||
task.status = deriveTaskStatus(task.subTasks);
|
||||
task.updatedAt = new Date().toISOString();
|
||||
// 进度更新不持久化,只在内存中更新以减少写盘
|
||||
};
|
||||
const nextSubTasks: SubTask[] = task.subTasks.map((subTask) => {
|
||||
if (subTask.id !== payload.subTaskId) return subTask;
|
||||
|
||||
const completeSubTask = (payload: { taskId: string; subTaskId: string; success: boolean; exitCode: number | null; error?: string }) => {
|
||||
const task = tasks.value.find((t) => t.id === payload.taskId);
|
||||
if (!task) return;
|
||||
const subTask = task.subTasks.find((s) => s.id === payload.subTaskId);
|
||||
if (!subTask) return;
|
||||
subTask.status = payload.success ? 'success' : 'failed';
|
||||
subTask.progress = payload.success ? 100 : subTask.progress;
|
||||
if (payload.error) subTask.error = payload.error;
|
||||
subTask.completedAt = new Date().toISOString();
|
||||
task.status = deriveTaskStatus(task.subTasks);
|
||||
task.updatedAt = new Date().toISOString();
|
||||
persist();
|
||||
};
|
||||
|
||||
const retryFailedSubTasks = async (taskId: string) => {
|
||||
const task = tasks.value.find((t) => t.id === taskId);
|
||||
if (!task) return;
|
||||
const failedSubTasks = task.subTasks.filter((s) => s.status === 'failed');
|
||||
if (failedSubTasks.length === 0) return;
|
||||
failedSubTasks.forEach((s) => {
|
||||
s.status = 'pending';
|
||||
s.progress = 0;
|
||||
s.message = '等待执行';
|
||||
s.stdoutTail = '';
|
||||
s.stderrTail = '';
|
||||
s.error = undefined;
|
||||
s.completedAt = undefined;
|
||||
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,
|
||||
};
|
||||
});
|
||||
task.status = 'pending';
|
||||
task.updatedAt = new Date().toISOString();
|
||||
persist();
|
||||
|
||||
// 重新触发主进程执行
|
||||
const options = {
|
||||
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: [], // 主进程会自行从 roomList 中查找,但这里可以传空;若主进程已支持 taskId 则无需重复传 roomList
|
||||
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,
|
||||
};
|
||||
// 由于当前 roomList 不在 store 中持久化,调用方(UI)应负责在重试时补充 roomList。
|
||||
// 为了兼容性,UI 层重试时直接调用 window.api.executeScript({ taskId, roomType, startTime, endTime, operation, roomList })
|
||||
// 本方法只负责重置状态并持久化,不直接调用 executeScript(避免 roomList 丢失)。
|
||||
// 若 UI 没有传入 roomList,当前主进程逻辑依赖它。建议 UI 层在调用 retry 后重新发起 executeScript。
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const removeTask = (taskId: string) => {
|
||||
tasks.value = tasks.value.filter((t) => t.id !== taskId);
|
||||
persist();
|
||||
};
|
||||
async function createAndExecuteTask(options: TaskOperationInput): Promise<{ task: Task; result: ExecuteTaskResult }> {
|
||||
const task = createTask(options);
|
||||
const result = await executeTask(task.id);
|
||||
return { task, result };
|
||||
}
|
||||
|
||||
const pendingTasks = computed(() => tasks.value.filter((t) => t.status === 'pending' || t.status === 'running'));
|
||||
const completedTasks = computed(() => tasks.value.filter((t) => t.status === 'success' || t.status === 'failed' || t.status === 'partial_failed'));
|
||||
|
||||
function deriveTaskStatus(subTasks: SubTask[]): Task['status'] {
|
||||
if (subTasks.every((s) => s.status === 'success')) return 'success';
|
||||
if (subTasks.every((s) => s.status === 'failed')) return 'failed';
|
||||
if (subTasks.some((s) => s.status === 'failed') && subTasks.some((s) => s.status === 'success')) return 'partial_failed';
|
||||
if (subTasks.some((s) => s.status === 'running')) return 'running';
|
||||
return 'pending';
|
||||
async function retryFailedSubTasks(taskId: string): Promise<ExecuteTaskResult> {
|
||||
const task = state.tasks.find((item) => item.id === taskId);
|
||||
if (!task) {
|
||||
return { success: false, error: '任务不存在,无法重试。' };
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
init,
|
||||
createTask,
|
||||
updateSubTaskProgress,
|
||||
completeSubTask,
|
||||
retryFailedSubTasks,
|
||||
removeTask,
|
||||
pendingTasks,
|
||||
completedTasks,
|
||||
};
|
||||
});
|
||||
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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user