# 任务列表(TaskList)实时显示脚本执行进度 — 实现思路与开发计划 > 基于 `zn-ai/electron/scripts` 中美团、抖音、飞猪等脚本,在渲染层任务列表组件中实时展示脚本执行过程,并支持任务状态流转与操作。 --- ## 一、现状梳理 | 模块 | 当前状态 | 关键文件 | |---|---|---| | 脚本执行 | 通过 `utilityProcess.fork` 串行执行,**阻塞式返回结果**,无实时推送 | `electron/service/execute-script-service/index.ts` | | 任务列表 UI | 使用 `@constant/task` **静态假数据** | `src/pages/home/components/TaskList.vue`、`TaskCard.vue` | | 脚本触发入口 | `TaskOperationDialog.vue` 中调用 `window.api.executeScript(options)` | `src/pages/home/components/TaskOperationDialog.vue` | | 状态管理 | 已有 `store/script.ts` 管理脚本元数据,**缺少任务(Task)生命周期管理** | `src/store/script.ts` | | IPC 通信 | 只有 Request/Response 模式(invoke/handle),**无主进程主动推送** | `electron/preload/index.ts` | --- ## 二、核心设计思路 ### 2.1 数据模型:Task(任务)+ SubTask(子任务) 一次"操作房型"的执行产生 **1 个 Task**,该 Task 包含 **N 个 SubTask**(对应美团、飞猪、抖音酒店、抖音温泉等脚本)。 ```ts // 子任务状态 type SubTaskStatus = 'pending' | 'running' | 'success' | 'failed'; interface SubTask { id: string; // 子任务唯一 ID taskId: string; // 所属主任务 ID scriptId: string; // 脚本标识,如 mt_trace.js name: string; // 渠道名称,如"美团房态追踪" status: SubTaskStatus; progress: number; // 0-100 message: string; // 当前执行步骤描述 stdoutTail: string; // 最新输出 stderrTail: string; // 最新错误 error?: string; // 失败原因 startedAt: string; completedAt?: string; } // 主任务状态 type TaskStatus = 'pending' | 'running' | 'success' | 'partial_failed' | 'failed'; interface Task { id: string; title: string; // 如"关闭渠道房型 - 大床房" operation: 'open' | 'close'; roomType: string; dateRange: [string, string]; status: TaskStatus; subTasks: SubTask[]; roomList: any[]; // 保留原始房型列表,用于重试时重新传参 createdAt: string; updatedAt: string; } ``` ### 2.2 实时进度推送方案 `utilityProcess.fork` 的 `stdout` 可以被主进程监听。采用 **"特殊前缀 JSON" + IPC 主动推送"** 的方案: 1. **脚本侧**:在执行关键步骤时输出进度日志 ```js console.log('__ZN_PROGRESS__' + JSON.stringify({ step: '正在登录美团后台', percent: 30 })); ``` 2. **主进程侧**:`execute-script-service` 解析 stdout,匹配 `__ZN_PROGRESS__` 前缀,emit 内部事件 3. **IPC 推送**:`runTaskOperationService.ts` 订阅内部事件,通过 `BrowserWindow.webContents.send('task:progress', payload)` 推送到渲染进程 > 如果暂时不修改脚本文件,也可以先实现"开始/完成"两个节点的推送,中间进度用 stdout 文本更新。 ### 2.3 状态流转与操作权限 | Tab | 包含状态 | 操作按钮 | |---|---|---| | **待处理** | `pending`、`running` | 查看(展开进度详情) | | **已处理** | `success`、`failed`、`partial_failed` | 成功 → **移除**;失败 / 部分失败 → **重试失败项** | **排队机制**:一个 Task 包含的 N 个 SubTask(对应各渠道脚本)在主进程中**串行排队执行**,避免并发启动多个浏览器实例导致资源冲突。 **重试粒度**:仅针对该 Task 下 `status === 'failed'` 的 SubTask 进行重试,成功的 SubTask 保持结果不变。 --- ## 三、分阶段开发计划 ### Phase 1:类型定义与 IPC 扩展 **目标**:打通主进程 -> 渲染进程的推送通道。 1. **`src/lib/task-types.ts`**(新建) - 定义 `Task`、`SubTask`、`TaskProgressPayload` 等接口。 2. **`src/lib/constants.ts`**(修改) - 新增 IPC 事件: ```ts TASK_PROGRESS = 'task:progress', TASK_STARTED = 'task:started', TASK_COMPLETED = 'task:completed', ``` 3. **`electron/preload/index.ts`**(修改) - 暴露监听接口: ```ts onTaskProgress: (cb) => ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, cb), onTaskStarted: (cb) => ipcRenderer.on(IPC_EVENTS.TASK_STARTED, cb), onTaskCompleted: (cb) => ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, cb), ``` 4. **`global.d.ts`**(修改) - 更新 `WindowApi` 类型声明。 --- ### Phase 2:主进程进度推送改造 **目标**:让脚本执行过程可被渲染层实时感知。 5. **`electron/service/execute-script-service/index.ts`**(修改) - 继承 `EventEmitter`,新增事件: - `progress`:解析到 `__ZN_PROGRESS__` 前缀时触发 - `stdout`:有新的标准输出时触发 - `stderr`:有新的错误输出时触发 - `executeScript` 方法签名扩展,增加 `taskId` / `subTaskId` 参数用于上下文绑定。 6. **`electron/process/runTaskOperationService.ts`**(修改) - `EXECUTE_SCRIPT` handler 改造: - 接收 `options.taskId`,生成 `subTaskId` 映射表 - 执行每个脚本前,发送 `TASK_STARTED` IPC - 订阅 `executeScriptServiceInstance` 的 `progress`/`stdout`/`stderr` 事件,组装 payload 后通过 `webContents.send(IPC_EVENTS.TASK_PROGRESS, ...)` 推送 - 脚本退出后发送 `TASK_COMPLETED` IPC --- ### Phase 3:渲染层任务状态管理 **目标**:集中管理任务生命周期。 7. **`src/store/task.ts`**(新建) ```ts export const useTaskStore = defineStore('task', () => { const tasks = ref([]); // 创建任务(在 TaskOperationDialog 确认时调用) const createTask = (options: ExecuteScriptOptions): Task => { ... }; // 更新子任务进度(监听 TASK_PROGRESS 时调用) const updateSubTaskProgress = (taskId, subTaskId, payload) => { ... }; // 完成子任务 const completeSubTask = (taskId, subTaskId, result) => { ... }; // 重试该任务下所有失败的子任务 const retryFailedSubTasks = async (taskId) => { ... }; // 移除已处理任务 const removeTask = (taskId) => { ... }; // Computed const pendingTasks = computed(() => ...); // pending + running const completedTasks = computed(() => ...); // success + failed return { ... }; }); ``` 8. **`src/App.vue` 或 `src/pages/home/index.vue`**(修改) - 应用挂载时注册 IPC 监听: ```ts onMounted(() => { window.api.onTaskProgress((_, payload) => taskStore.updateSubTaskProgress(...)); window.api.onTaskStarted((_, payload) => ...); window.api.onTaskCompleted((_, payload) => taskStore.completeSubTask(...)); }); ``` --- ### Phase 4:UI 组件改造 **目标**:将假数据替换为真实任务数据,支持 tab 切换与操作。 9. **`src/pages/home/components/TaskCard.vue`**(修改) - `props` 改为接收 `Task` 或 `SubTask` 对象 - 根据 `status` 渲染不同状态标签(`warning` 运行中 / `error` 失败 / `success` 成功) - 按钮逻辑: - `running` → "查看"(可展开显示 stdoutTail) - `failed` / `partial_failed` → "重试失败项"(emit `retry-failed`) - `success` → "移除"(emit `remove`) - 显示进度条(`el-progress` 或自定义 div) 10. **`src/pages/home/components/TaskList.vue`**(修改) - 从 `useTaskStore()` 读取任务列表 - "待处理" tab 显示 `pendingTasks`,"已处理" tab 显示 `completedTasks` - 动态计算 `total` 数量 - 顶部时间区域改为展示脚本执行的数据时间段:左侧基于最新任务的 `createdAt` 显示 `"今天"`、`"昨天"` 或具体日期(`MM/DD`);右侧显示该任务的 `dateRange`(格式 `YYYY-MM-DD ~ YYYY-MM-DD`)。无任务时显示 `"执行时段"` 和 `"--"`。移除原有的 `setInterval` 实时定时器。 - 处理 `retry-failed`(调用 `taskStore.retryFailedSubTasks(taskId)`)/ `remove` 事件 --- ### Phase 5:调用点接入 **目标**:让用户在对话框确认后,任务立刻出现在列表中并开始推送进度。 11. **`src/pages/home/components/TaskOperationDialog.vue`**(修改) - `confirm` 方法中,在调用 `window.api.executeScript(options)` 之前: ```ts const task = taskStore.createTask(options); options.taskId = task.id; window.api.executeScript(options); ``` 12. **`src/pages/scripts/index.vue`**(可选) - 脚本管理页的"测试运行"也可接入任务列表(通过 `SCRIPT_RUN` IPC),保持体验一致性。 --- ### Phase 6:脚本输出执行步骤(可选但推荐) **目标**:让子任务进度条真正动起来。 13. **`electron/scripts/mt_trace.js`、`fg_trace.js`、`dy_hotel_trace.js`、`dy_hot_spring_trace.js`**(修改) - 在关键步骤插入进度输出: ```js function reportProgress(step, percent) { console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent })); } // 示例 reportProgress('连接本地浏览器', 10); reportProgress('定位目标页面', 30); reportProgress('操作房态数据', 60); reportProgress('保存并校验', 90); ``` --- ## 四、关键数据流 ### 首次执行 ``` 用户点击确认 ↓ TaskOperationDialog 调用 taskStore.createTask() → 生成 taskId ↓ window.api.executeScript({ taskId, roomType, startTime, endTime, operation }) ↓ 主进程 EXECUTE_SCRIPT handler ├─ 发送 IPC task:started ├─ for 循环串行执行每个脚本(utilityProcess.fork) │ ├─ 脚本 stdout → 解析 __ZN_PROGRESS__ → 发送 IPC task:progress │ └─ 脚本 exit → 发送 IPC task:completed ↓ 渲染层 Store 接收 IPC → 更新 tasks 数组 ↓ List.vue / Card.vue 响应式更新 UI ``` ### 重试失败项 ``` 用户点击"重试失败项" ↓ List.vue 调用 taskStore.retryFailedSubTasks(taskId) ├─ 将该 Task 下所有 failed 的 SubTask 重置为 pending ├─ Task 状态回退为 pending / running └─ 重新调用 window.api.executeScript({ taskId, roomType, startTime, endTime, operation, roomList }) ↓ 主进程只执行 status === pending 的 SubTask(串行排队) ↓ 执行完成后更新 Task 状态,移回"已处理" Tab ``` > **注意**:重试时需要 `roomList` 参数。`createTask` 会将 `options.roomList` 存入 `Task.roomList` 并随任务列表一起持久化到 `electron-store`,确保刷新后重试仍能拿到完整参数。 --- ## 五、文件变更清单汇总 | 新建文件 | 说明 | |---|---| | `src/lib/task-types.ts` | Task / SubTask 类型定义 | | `src/store/task.ts` | 任务状态管理 Pinia Store | | 修改文件 | 说明 | |---|---| | `src/lib/constants.ts` | 新增 `TASK_PROGRESS` 等 IPC 常量 | | `electron/preload/index.ts` | 暴露 `onTaskProgress` 等监听 API | | `global.d.ts` | 更新 `WindowApi` 类型 | | `electron/service/execute-script-service/index.ts` | 解析进度并 emit 事件 | | `electron/process/runTaskOperationService.ts` | 绑定 taskId 并推送 IPC | | `src/pages/home/components/TaskList.vue` | 接入真实数据、tab 过滤、日期时间动态化 | | `src/pages/home/components/TaskCard.vue` | 动态状态、进度、操作按钮 | | `src/pages/home/components/TaskOperationDialog.vue` | 执行前创建 Task | | `electron/scripts/*.js` | 插入 `__ZN_PROGRESS__` 输出(可选) | --- ## 六、调整说明(2026-04-16) 针对实现边界补充以下确认: 1. **排队机制**:一个 Task 内的多个 SubTask(各渠道脚本)在主进程中**串行排队执行**,不会并发启动多个浏览器实例。 2. **重试粒度**:"重试"操作仅针对该 Task 下 `status === 'failed'` 的 SubTask,成功的 SubTask 保持原结果不变。 3. **Store 方法**:`src/store/task.ts` 中增加 `retryFailedSubTasks(taskId)` 方法,用于批量重置并重新触发失败子任务。 4. **视觉 UI 延用当前**:`TaskList.vue` 与 `TaskCard.vue` 保持现有样式和布局,仅将数据来源从 `@constant/task` 静态假数据替换为 `useTaskStore`,并在现有样式框架内绑定状态、进度与操作按钮。 5. **顶部时间区域显示脚本执行时段**:`TaskList.vue` 顶部不再使用 `setInterval` 实时显示当前时间,而是展示当前 Tab 下最新任务的执行日期范围: - 左侧日期标签根据最新任务的 `createdAt` 自动判断显示 **"今天"**、**"昨天"** 或具体日期(如 `04/16`)。 - 右侧显示该任务的 `dateRange`,格式为 `YYYY-MM-DD ~ YYYY-MM-DD`,用于记录脚本操作房型的起止时间段。 - 当列表中没有任务时,左侧显示 `"执行时段"`,右侧显示 `"--"`。 6. **数据持久化使用 `electron-store`**: - 渲染层 Store `useTaskStore` 通过 IPC (`GET_CONFIG` / `SET_CONFIG`) 读写任务列表,避免主进程直接暴露 Store 实例到渲染层。 - 主进程在 `config-service` 的 `DEFAULT_CONFIG` 中新增 `CONFIG_KEYS.TASK_LIST`,默认值为空数组 `[]`。 - `useTaskStore` 初始化时异步加载已持久化的任务列表;每次 `createTask`、`completeSubTask`、`removeTask` 等变更操作后,通过 `window.api.invoke(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.TASK_LIST, tasks.value)` 同步到 `electron-store`。 - 注意:持久化数据为任务列表(含 SubTask 状态),实时进度更新(`task:progress`)频繁变化时可**仅更新内存状态**,待 `task:completed` 后再统一持久化,以减少写盘次数。 --- ## 七、Sub-agent 开发分工 共启动 **4 个 sub-agent** 按流水线并行开发。 | Sub-agent | 负责阶段 | 关键文件 | 依赖 | |---|---|---|---| | **SA-1 主进程** | Phase 1 + Phase 2 | `src/lib/task-types.ts`、`src/lib/constants.ts`、`electron/preload/index.ts`、`global.d.ts`、`electron/service/execute-script-service/index.ts`、`electron/process/runTaskOperationService.ts` | 无 | | **SA-2 状态管理** | Phase 3 | `src/store/task.ts`(新建)、`src/App.vue`(挂载监听) | 需 SA-1 的类型与 IPC 契约 | | **SA-3 前端 UI** | Phase 4 + Phase 5 | `src/pages/home/components/TaskList.vue`、`TaskCard.vue`、`TaskOperationDialog.vue` | 需 SA-2 的 Store API(可按本计划接口契约先行开发) | | **SA-4 脚本进度** | Phase 6 | `electron/scripts/mt_trace.js`、`fg_trace.js`、`dy_hotel_trace.js`、`dy_hot_spring_trace.js` | 无 | ### 各 Sub-agent 验收标准 **SA-1**: - `executeScriptService` 继承 `EventEmitter`,能解析 `__ZN_PROGRESS__` 前缀并触发 `progress`/`stdout`/`stderr` 事件。 - `runTaskOperationService` 的 `EXECUTE_SCRIPT` handler 接收 `options.taskId`,为每个脚本生成 `subTaskId`,串行执行并推送 `task:started` / `task:progress` / `task:completed` IPC 事件。 **SA-2**: - `useTaskStore` 通过 `electron-store` 持久化任务列表(IPC 读写)。 - 提供 `createTask`、`updateSubTaskProgress`、`completeSubTask`、`retryFailedSubTasks`、`removeTask`。 - `pendingTasks` / `completedTasks` computed 正确过滤。 **SA-3**: - `TaskList.vue` / `TaskCard.vue` 接入 `useTaskStore`,Tab 数量和任务数量动态计算。 - `running` 状态显示进度条和最新日志展开;`failed` / `partial_failed` 显示"重试失败项";`success` 显示"移除"。 - `TaskOperationDialog.vue` 确认时先 `taskStore.createTask(options)`,再 `window.api.executeScript(options)`。 **SA-4**: - 每个脚本在关键步骤输出 `__ZN_PROGRESS__{"step":"...","percent":N}`。 - 不影响原有脚本逻辑。