Files
zn-ai/docs/TaskList-Implementation-Plan.md
2026-04-14 17:02:20 +08:00

299 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 任务列表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 4UI 组件改造
**目标**:将假数据替换为真实任务数据,支持 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)` 方法,用于批量重置并重新触发失败子任务。