299 lines
11 KiB
Markdown
299 lines
11 KiB
Markdown
# 任务列表(TaskList)实时显示脚本执行进度 — 实现思路与开发计划
|
||
|
||
> 基于 `zn-ai/electron/scripts` 中美团、抖音、飞猪等脚本,在渲染层任务列表组件中实时展示脚本执行过程,并支持任务状态流转与操作。
|
||
|
||
---
|
||
|
||
## 一、现状梳理
|
||
|
||
| 模块 | 当前状态 | 关键文件 |
|
||
|---|---|---|
|
||
| 脚本执行 | 通过 `utilityProcess.fork` 串行执行,**阻塞式返回结果**,无实时推送 | `electron/service/execute-script-service/index.ts` |
|
||
| 任务列表 UI | 使用 `@constant/task` **静态假数据** | `src/components/TaskList/List.vue`、`Card.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[];
|
||
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<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 { ... };
|
||
});
|
||
```
|
||
|
||
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/components/TaskList/Card.vue`**(修改)
|
||
- `props` 改为接收 `Task` 或 `SubTask` 对象
|
||
- 根据 `status` 渲染不同状态标签(`warning` 运行中 / `error` 失败 / `success` 成功)
|
||
- 按钮逻辑:
|
||
- `running` → "查看"(可展开显示 stdoutTail)
|
||
- `failed` / `partial_failed` → "重试失败项"(emit `retry-failed`)
|
||
- `success` → "移除"(emit `remove`)
|
||
- 显示进度条(`el-progress` 或自定义 div)
|
||
|
||
10. **`src/components/TaskList/List.vue`**(修改)
|
||
- 从 `useTaskStore()` 读取任务列表
|
||
- "待处理" tab 显示 `pendingTasks`,"已处理" tab 显示 `completedTasks`
|
||
- 动态计算 `total` 数量
|
||
- 处理 `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, ... })
|
||
↓
|
||
主进程只执行 status === pending 的 SubTask(串行排队)
|
||
↓
|
||
执行完成后更新 Task 状态,移回"已处理" Tab
|
||
```
|
||
|
||
---
|
||
|
||
## 五、文件变更清单汇总
|
||
|
||
| 新建文件 | 说明 |
|
||
|---|---|
|
||
| `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/components/TaskList/List.vue` | 接入真实数据与 tab 过滤 |
|
||
| `src/components/TaskList/Card.vue` | 动态状态、进度、操作按钮 |
|
||
| `src/pages/home/components/TaskOperationDialog.vue` | 执行前创建 Task |
|
||
| `electron/scripts/*.js` | 插入 `__ZN_PROGRESS__` 输出(可选) |
|
||
|
||
---
|
||
|
||
## 六、调整说明(2026-04-14)
|
||
|
||
针对实现边界补充以下确认:
|
||
|
||
1. **排队机制**:一个 Task 内的多个 SubTask(各渠道脚本)在主进程中**串行排队执行**,不会并发启动多个浏览器实例。
|
||
2. **重试粒度**:"重试"操作仅针对该 Task 下 `status === 'failed'` 的 SubTask,成功的 SubTask 保持原结果不变。
|
||
3. **Store 方法**:`src/store/task.ts` 中增加 `retryFailedSubTasks(taskId)` 方法,用于批量重置并重新触发失败子任务。
|