16 KiB
任务列表(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(对应美团、飞猪、抖音酒店、抖音温泉等脚本)。
// 子任务状态
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 主动推送" 的方案:
- 脚本侧:在执行关键步骤时输出进度日志
console.log('__ZN_PROGRESS__' + JSON.stringify({ step: '正在登录美团后台', percent: 30 })); - 主进程侧:
execute-script-service解析 stdout,匹配__ZN_PROGRESS__前缀,emit 内部事件 - 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 扩展
目标:打通主进程 -> 渲染进程的推送通道。
-
src/lib/task-types.ts(新建)- 定义
Task、SubTask、TaskProgressPayload等接口。
- 定义
-
src/lib/constants.ts(修改)- 新增 IPC 事件:
TASK_PROGRESS = 'task:progress', TASK_STARTED = 'task:started', TASK_COMPLETED = 'task:completed',
- 新增 IPC 事件:
-
electron/preload/index.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),
- 暴露监听接口:
-
global.d.ts(修改)- 更新
WindowApi类型声明。
- 更新
Phase 2:主进程进度推送改造
目标:让脚本执行过程可被渲染层实时感知。
-
electron/service/execute-script-service/index.ts(修改)- 继承
EventEmitter,新增事件:progress:解析到__ZN_PROGRESS__前缀时触发stdout:有新的标准输出时触发stderr:有新的错误输出时触发
executeScript方法签名扩展,增加taskId/subTaskId参数用于上下文绑定。
- 继承
-
electron/process/runTaskOperationService.ts(修改)EXECUTE_SCRIPThandler 改造:- 接收
options.taskId,生成subTaskId映射表 - 执行每个脚本前,发送
TASK_STARTEDIPC - 订阅
executeScriptServiceInstance的progress/stdout/stderr事件,组装 payload 后通过webContents.send(IPC_EVENTS.TASK_PROGRESS, ...)推送 - 脚本退出后发送
TASK_COMPLETEDIPC
- 接收
Phase 3:渲染层任务状态管理
目标:集中管理任务生命周期。
-
src/store/task.ts(新建)export const useTaskStore = defineStore('task', () => { const tasks = ref<Task[]>([]); // 创建任务(在 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 { ... }; }); -
src/App.vue或src/pages/home/index.vue(修改)- 应用挂载时注册 IPC 监听:
onMounted(() => { window.api.onTaskProgress((_, payload) => taskStore.updateSubTaskProgress(...)); window.api.onTaskStarted((_, payload) => ...); window.api.onTaskCompleted((_, payload) => taskStore.completeSubTask(...)); });
- 应用挂载时注册 IPC 监听:
Phase 4:UI 组件改造
目标:将假数据替换为真实任务数据,支持 tab 切换与操作。
-
src/pages/home/components/TaskCard.vue(修改)props改为接收Task或SubTask对象- 根据
status渲染不同状态标签(warning运行中 /error失败 /success成功) - 按钮逻辑:
running→ "查看"(可展开显示 stdoutTail)failed/partial_failed→ "重试失败项"(emitretry-failed)success→ "移除"(emitremove)
- 显示进度条(
el-progress或自定义 div)
-
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:调用点接入
目标:让用户在对话框确认后,任务立刻出现在列表中并开始推送进度。
-
src/pages/home/components/TaskOperationDialog.vue(修改)confirm方法中,在调用window.api.executeScript(options)之前:const task = taskStore.createTask(options); options.taskId = task.id; window.api.executeScript(options);
-
src/pages/scripts/index.vue(可选)- 脚本管理页的"测试运行"也可接入任务列表(通过
SCRIPT_RUNIPC),保持体验一致性。
- 脚本管理页的"测试运行"也可接入任务列表(通过
Phase 6:脚本输出执行步骤(可选但推荐)
目标:让子任务进度条真正动起来。
electron/scripts/mt_trace.js、fg_trace.js、dy_hotel_trace.js、dy_hot_spring_trace.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)
针对实现边界补充以下确认:
- 排队机制:一个 Task 内的多个 SubTask(各渠道脚本)在主进程中串行排队执行,不会并发启动多个浏览器实例。
- 重试粒度:"重试"操作仅针对该 Task 下
status === 'failed'的 SubTask,成功的 SubTask 保持原结果不变。 - Store 方法:
src/store/task.ts中增加retryFailedSubTasks(taskId)方法,用于批量重置并重新触发失败子任务。 - 视觉 UI 延用当前:
TaskList.vue与TaskCard.vue保持现有样式和布局,仅将数据来源从@constant/task静态假数据替换为useTaskStore,并在现有样式框架内绑定状态、进度与操作按钮。 - 顶部时间区域显示脚本执行时段:
TaskList.vue顶部不再使用setInterval实时显示当前时间,而是展示当前 Tab 下最新任务的执行日期范围:- 左侧日期标签根据最新任务的
createdAt自动判断显示 "今天"、"昨天" 或具体日期(如04/16)。 - 右侧显示该任务的
dateRange,格式为YYYY-MM-DD ~ YYYY-MM-DD,用于记录脚本操作房型的起止时间段。 - 当列表中没有任务时,左侧显示
"执行时段",右侧显示"--"。
- 左侧日期标签根据最新任务的
- 数据持久化使用
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后再统一持久化,以减少写盘次数。
- 渲染层 Store
七、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_SCRIPThandler 接收options.taskId,为每个脚本生成subTaskId,串行执行并推送task:started/task:progress/task:completedIPC 事件。
SA-2:
useTaskStore通过electron-store持久化任务列表(IPC 读写)。- 提供
createTask、updateSubTaskProgress、completeSubTask、retryFailedSubTasks、removeTask。 pendingTasks/completedTaskscomputed 正确过滤。
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}。 - 不影响原有脚本逻辑。