From 210e8eb36395f7eb0f656ef4a0c577e9e75604ef Mon Sep 17 00:00:00 2001 From: DEV_DSW <562304744@qq.com> Date: Thu, 16 Apr 2026 16:59:49 +0800 Subject: [PATCH] feat: add task management and progress reporting - Implemented task and subtask structures with progress tracking. - Added reporting functionality to log progress at various stages in hotel room status management scripts. - Created a task store to manage tasks and their states, including persistence to local storage. - Updated UI components to display task lists and handle task actions (retry, remove). - Removed deprecated TaskCard and TaskList components, replacing them with a new structure for better maintainability. - Enhanced script execution service to emit progress events for UI updates. --- dist-electron/main/main.js | 2 +- dist-electron/preload/preload.js | 19 ++ docs/TaskList-Implementation-Plan.md | 61 ++++++- plan.md => docs/plan.md | 0 docs/todo-list.md | 7 + electron/preload/index.ts | 17 ++ electron/process/runTaskOperationService.ts | 92 +++++++--- electron/scripts/dy_hot_spring_trace.js | 10 ++ electron/scripts/dy_hotel_trace.js | 10 ++ electron/scripts/fg_trace.js | 13 +- electron/scripts/mt_trace.js | 12 +- electron/service/config-service/index.ts | 1 + .../service/execute-script-service/index.ts | 17 ++ global.d.ts | 9 + src/components/TaskList/Card.vue | 63 ------- src/components/TaskList/index.vue | 77 -------- src/lib/constants.ts | 6 + src/lib/task-types.ts | 40 +++++ src/pages/home/components/Task.vue | 65 ------- src/pages/home/components/TaskCard.vue | 161 +++++++++++++++++ src/pages/home/components/TaskList.vue | 163 +++++++++++++++++ .../home/components/TaskOperationDialog.vue | 5 + src/pages/home/index.vue | 11 +- src/stores/task.ts | 164 ++++++++++++++++++ 24 files changed, 788 insertions(+), 237 deletions(-) rename plan.md => docs/plan.md (100%) create mode 100644 docs/todo-list.md delete mode 100644 src/components/TaskList/Card.vue delete mode 100644 src/components/TaskList/index.vue create mode 100644 src/lib/task-types.ts delete mode 100644 src/pages/home/components/Task.vue create mode 100644 src/pages/home/components/TaskCard.vue create mode 100644 src/pages/home/components/TaskList.vue create mode 100644 src/stores/task.ts diff --git a/dist-electron/main/main.js b/dist-electron/main/main.js index 1b9d75f..48b2026 100644 --- a/dist-electron/main/main.js +++ b/dist-electron/main/main.js @@ -1,6 +1,6 @@ "use strict"; require("electron"); -require("./main-BekteP6H.js"); +require("./main-ByCp1zrw.js"); require("electron-squirrel-startup"); require("electron-log"); require("bytenode"); diff --git a/dist-electron/preload/preload.js b/dist-electron/preload/preload.js index d7b783e..c4f6639 100644 --- a/dist-electron/preload/preload.js +++ b/dist-electron/preload/preload.js @@ -41,6 +41,9 @@ var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => { IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme"; IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated"; IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script"; + IPC_EVENTS2["TASK_PROGRESS"] = "task:progress"; + IPC_EVENTS2["TASK_STARTED"] = "task:started"; + IPC_EVENTS2["TASK_COMPLETED"] = "task:completed"; IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel"; IPC_EVENTS2["SCRIPT_LIST"] = "script:list"; IPC_EVENTS2["SCRIPT_GET"] = "script:get"; @@ -100,6 +103,22 @@ const api = { }, // 执行脚本 executeScript: (params) => electron.ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params), + // 任务事件 + onTaskProgress: (cb) => { + const subscription = (_event, payload) => cb(payload); + electron.ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, subscription); + return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_PROGRESS, subscription); + }, + onTaskStarted: (cb) => { + const subscription = (_event, payload) => cb(payload); + electron.ipcRenderer.on(IPC_EVENTS.TASK_STARTED, subscription); + return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_STARTED, subscription); + }, + onTaskCompleted: (cb) => { + const subscription = (_event, payload) => cb(payload); + electron.ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, subscription); + return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_COMPLETED, subscription); + }, // 打开渠道 openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels), // 脚本管理 diff --git a/docs/TaskList-Implementation-Plan.md b/docs/TaskList-Implementation-Plan.md index 6268508..720640f 100644 --- a/docs/TaskList-Implementation-Plan.md +++ b/docs/TaskList-Implementation-Plan.md @@ -9,7 +9,7 @@ | 模块 | 当前状态 | 关键文件 | |---|---|---| | 脚本执行 | 通过 `utilityProcess.fork` 串行执行,**阻塞式返回结果**,无实时推送 | `electron/service/execute-script-service/index.ts` | -| 任务列表 UI | 使用 `@constant/task` **静态假数据** | `src/components/TaskList/List.vue`、`Card.vue` | +| 任务列表 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` | @@ -52,6 +52,7 @@ interface Task { dateRange: [string, string]; status: TaskStatus; subTasks: SubTask[]; + roomList: any[]; // 保留原始房型列表,用于重试时重新传参 createdAt: string; updatedAt: string; } @@ -177,7 +178,7 @@ interface Task { ### Phase 4:UI 组件改造 **目标**:将假数据替换为真实任务数据,支持 tab 切换与操作。 -9. **`src/components/TaskList/Card.vue`**(修改) +9. **`src/pages/home/components/TaskCard.vue`**(修改) - `props` 改为接收 `Task` 或 `SubTask` 对象 - 根据 `status` 渲染不同状态标签(`warning` 运行中 / `error` 失败 / `success` 成功) - 按钮逻辑: @@ -186,10 +187,11 @@ interface Task { - `success` → "移除"(emit `remove`) - 显示进度条(`el-progress` 或自定义 div) -10. **`src/components/TaskList/List.vue`**(修改) +10. **`src/pages/home/components/TaskList.vue`**(修改) - 从 `useTaskStore()` 读取任务列表 - "待处理" tab 显示 `pendingTasks`,"已处理" tab 显示 `completedTasks` - 动态计算 `total` 数量 + - 顶部日期时间实时动态化("今天"/"昨天"/具体日期 + `HH:mm:ss`) - 处理 `retry-failed`(调用 `taskStore.retryFailedSubTasks(taskId)`)/ `remove` 事件 --- @@ -259,13 +261,15 @@ List.vue / Card.vue 响应式更新 UI List.vue 调用 taskStore.retryFailedSubTasks(taskId) ├─ 将该 Task 下所有 failed 的 SubTask 重置为 pending ├─ Task 状态回退为 pending / running - └─ 重新调用 window.api.executeScript({ taskId, ... }) + └─ 重新调用 window.api.executeScript({ taskId, roomType, startTime, endTime, operation, roomList }) ↓ 主进程只执行 status === pending 的 SubTask(串行排队) ↓ 执行完成后更新 Task 状态,移回"已处理" Tab ``` +> **注意**:重试时需要 `roomList` 参数。`createTask` 会将 `options.roomList` 存入 `Task.roomList` 并随任务列表一起持久化到 `electron-store`,确保刷新后重试仍能拿到完整参数。 + --- ## 五、文件变更清单汇总 @@ -282,17 +286,60 @@ List.vue 调用 taskStore.retryFailedSubTasks(taskId) | `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/TaskList.vue` | 接入真实数据、tab 过滤、日期时间动态化 | +| `src/pages/home/components/TaskCard.vue` | 动态状态、进度、操作按钮 | | `src/pages/home/components/TaskOperationDialog.vue` | 执行前创建 Task | | `electron/scripts/*.js` | 插入 `__ZN_PROGRESS__` 输出(可选) | --- -## 六、调整说明(2026-04-14) +## 六、调整说明(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` 顶部原有的静态日期("今天")和时间("02:32:05")改为响应式实时显示: + - 左侧日期标签根据当前日期自动判断显示 **"今天"**、**"昨天"** 或具体日期(如 `04/16`)。 + - 右侧时间通过 `setInterval` 每秒更新,格式为 `HH:mm:ss`。 + - 组件卸载时自动清理定时器。 +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}`。 +- 不影响原有脚本逻辑。 diff --git a/plan.md b/docs/plan.md similarity index 100% rename from plan.md rename to docs/plan.md diff --git a/docs/todo-list.md b/docs/todo-list.md new file mode 100644 index 0000000..a89e06e --- /dev/null +++ b/docs/todo-list.md @@ -0,0 +1,7 @@ +# 功能清单 + +1、任务列表 +2、走本地模型配置,重构模型对话功能 - 完成 +3、上传表单信息+读取信息,脚本执行录取表单 +4、定时任务脚本关联多个脚本执行 +5、一键打开渠道可以新增渠道 - 完成 \ No newline at end of file diff --git a/electron/preload/index.ts b/electron/preload/index.ts index cf02fd3..bc3e1ca 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -55,6 +55,23 @@ const api: WindowApi = { // 执行脚本 executeScript: (params: any) => ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params), + // 任务事件 + onTaskProgress: (cb: (payload: any) => void) => { + const subscription = (_event: any, payload: any) => cb(payload); + ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, subscription); + return () => ipcRenderer.removeListener(IPC_EVENTS.TASK_PROGRESS, subscription); + }, + onTaskStarted: (cb: (payload: any) => void) => { + const subscription = (_event: any, payload: any) => cb(payload); + ipcRenderer.on(IPC_EVENTS.TASK_STARTED, subscription); + return () => ipcRenderer.removeListener(IPC_EVENTS.TASK_STARTED, subscription); + }, + onTaskCompleted: (cb: (payload: any) => void) => { + const subscription = (_event: any, payload: any) => cb(payload); + ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, subscription); + return () => ipcRenderer.removeListener(IPC_EVENTS.TASK_COMPLETED, subscription); + }, + // 打开渠道 openChannel: (channels: any) => ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels), diff --git a/electron/process/runTaskOperationService.ts b/electron/process/runTaskOperationService.ts index fb90c11..b16f7e7 100644 --- a/electron/process/runTaskOperationService.ts +++ b/electron/process/runTaskOperationService.ts @@ -1,5 +1,5 @@ -import { ipcMain, app } from 'electron'; +import { ipcMain, app, BrowserWindow } from 'electron'; import { IPC_EVENTS } from '@lib/constants'; import { launchLocalChrome } from '@electron/utils/chrome/launchLocalChrome' import { executeScriptService } from '@electron/service/execute-script-service'; @@ -15,6 +15,7 @@ import fs from 'fs' import path from 'path' import { spawn } from 'child_process' import log from 'electron-log'; +import { randomUUID } from 'crypto'; const openedTabIndexByChannelName = new Map() @@ -228,7 +229,7 @@ export function runTaskOperationService() { // 执行脚本 ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (_event, options: any) => { try { - // 从options.roomList列表中找到对应的名称 + const taskId = options.taskId || randomUUID(); const roomType = options.roomList.find((item: any) => item.id === options.roomType); const pairs: Array<[string, string]> = [ @@ -246,33 +247,82 @@ export function runTaskOperationService() { if (!fs.existsSync(p)) { throw new Error(`Script not found for channel ${channel}: ${p}`) } - return { channel, scriptPath: p } + return { channel, fileName, scriptPath: p } }) + const channelNameMap: Record = { + fzName: 'fliggy', + mtName: 'meituan', + dyHotelName: 'douyin', + dyHotSpringName: 'douyin', + } + const defaultTabIndexMap: Record = { + fliggy: 0, + meituan: 1, + douyin: 2 + } + const results: any[] = [] for (let i = 0; i < scriptPaths.length; i++) { const item = scriptPaths[i] - const channelNameMap: Record = { - fzName: 'fliggy', - mtName: 'meituan', - dyHotelName: 'douyin', - dyHotSpringName: 'douyin', - } - const defaultTabIndexMap: Record = { - fliggy: 0, - meituan: 1, - douyin: 2 - } + const subTaskId = `${taskId}_${item.channel}`; const mappedName = channelNameMap[item.channel] const tabIndex = mappedName ? (openedTabIndexByChannelName.get(mappedName) ?? defaultTabIndexMap[mappedName] ?? i) : i + + const win = BrowserWindow.getAllWindows()[0]; + win?.webContents.send(IPC_EVENTS.TASK_STARTED, { + taskId, + subTaskId, + scriptId: item.fileName, + name: item.channel, + }); + + const onProgress = (payload: any) => { + win?.webContents.send(IPC_EVENTS.TASK_PROGRESS, payload); + }; + const onStdout = (payload: any) => { + win?.webContents.send(IPC_EVENTS.TASK_PROGRESS, { + ...payload, + stdoutTail: payload.text, + }); + }; + const onStderr = (payload: any) => { + win?.webContents.send(IPC_EVENTS.TASK_PROGRESS, { + ...payload, + stderrTail: payload.text, + }); + }; + + executeScriptServiceInstance.on('progress', onProgress); + executeScriptServiceInstance.on('stdout', onStdout); + executeScriptServiceInstance.on('stderr', onStderr); + log.info(`Launching script for channel ${item.channel}: ${item.scriptPath} (tabIndex: ${tabIndex})`) - const result = await executeScriptServiceInstance.executeScript(item.scriptPath, { - roomType: roomType[item.channel], - startTime: options.startTime, - endTime: options.endTime, - operation: options.operation, - tabIndex, - }) + const result = await executeScriptServiceInstance.executeScript( + item.scriptPath, + { + roomType: roomType[item.channel], + startTime: options.startTime, + endTime: options.endTime, + operation: options.operation, + tabIndex, + }, + taskId, + subTaskId, + ); + + executeScriptServiceInstance.off('progress', onProgress); + executeScriptServiceInstance.off('stdout', onStdout); + executeScriptServiceInstance.off('stderr', onStderr); + + win?.webContents.send(IPC_EVENTS.TASK_COMPLETED, { + taskId, + subTaskId, + success: result.success, + exitCode: result.exitCode, + error: result.error, + }); + results.push({ channel: item.channel, scriptPath: item.scriptPath, diff --git a/electron/scripts/dy_hot_spring_trace.js b/electron/scripts/dy_hot_spring_trace.js index cb4c322..13fc068 100644 --- a/electron/scripts/dy_hot_spring_trace.js +++ b/electron/scripts/dy_hot_spring_trace.js @@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js'; const { preparePage, safeDisconnectBrowser } = tabsPkg; +function reportProgress(step, percent) { + console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent })); +} + const parseDateInput = (dateStr) => { if (!dateStr) return null; if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr); @@ -158,6 +162,7 @@ const navigateToRoomStatusManagement = async (page) => { let browser; try { + reportProgress('连接本地浏览器', 10); const groupId = '1816249020842116'; const homeUrl = `https://life.douyin.com/p/home?groupid=${groupId}`; const priceAmountStateUrl = `https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=${groupId}`; @@ -175,6 +180,7 @@ const navigateToRoomStatusManagement = async (page) => { } catch (e2) {} } + reportProgress('定位目标页面', 30); // Navigation logic (User provided) try { // Try to click the store/login button if visible @@ -199,12 +205,14 @@ const navigateToRoomStatusManagement = async (page) => { if (!roomType || !startDate) { log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.'); + reportProgress('执行完成', 100); return; } // Wait for table rows to appear await page.locator('.lifep-table-row').first().waitFor({ state: 'visible', timeout: 15000 }); + reportProgress('操作房态数据', 60); const dateList = buildDateList(startDate, endDate); for (const dateStr of dateList) { try { @@ -215,11 +223,13 @@ const navigateToRoomStatusManagement = async (page) => { await page.waitForTimeout(500 + Math.random() * 500); } + reportProgress('保存并校验', 90); } catch (error) { log.error(error); process.exitCode = 1; } finally { await safeDisconnectBrowser(browser); + reportProgress('执行完成', 100); process.exit(process.exitCode || 0); } })(); diff --git a/electron/scripts/dy_hotel_trace.js b/electron/scripts/dy_hotel_trace.js index cb4c322..13fc068 100644 --- a/electron/scripts/dy_hotel_trace.js +++ b/electron/scripts/dy_hotel_trace.js @@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js'; const { preparePage, safeDisconnectBrowser } = tabsPkg; +function reportProgress(step, percent) { + console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent })); +} + const parseDateInput = (dateStr) => { if (!dateStr) return null; if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr); @@ -158,6 +162,7 @@ const navigateToRoomStatusManagement = async (page) => { let browser; try { + reportProgress('连接本地浏览器', 10); const groupId = '1816249020842116'; const homeUrl = `https://life.douyin.com/p/home?groupid=${groupId}`; const priceAmountStateUrl = `https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=${groupId}`; @@ -175,6 +180,7 @@ const navigateToRoomStatusManagement = async (page) => { } catch (e2) {} } + reportProgress('定位目标页面', 30); // Navigation logic (User provided) try { // Try to click the store/login button if visible @@ -199,12 +205,14 @@ const navigateToRoomStatusManagement = async (page) => { if (!roomType || !startDate) { log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.'); + reportProgress('执行完成', 100); return; } // Wait for table rows to appear await page.locator('.lifep-table-row').first().waitFor({ state: 'visible', timeout: 15000 }); + reportProgress('操作房态数据', 60); const dateList = buildDateList(startDate, endDate); for (const dateStr of dateList) { try { @@ -215,11 +223,13 @@ const navigateToRoomStatusManagement = async (page) => { await page.waitForTimeout(500 + Math.random() * 500); } + reportProgress('保存并校验', 90); } catch (error) { log.error(error); process.exitCode = 1; } finally { await safeDisconnectBrowser(browser); + reportProgress('执行完成', 100); process.exit(process.exitCode || 0); } })(); diff --git a/electron/scripts/fg_trace.js b/electron/scripts/fg_trace.js index 854123e..27c3a2b 100644 --- a/electron/scripts/fg_trace.js +++ b/electron/scripts/fg_trace.js @@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js'; const { preparePage, safeDisconnectBrowser } = tabsPkg; +function reportProgress(step, percent) { + console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent })); +} + const parseDateInput = (dateStr) => { if (!dateStr) return null; if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr); @@ -137,6 +141,7 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation let browser; try { + reportProgress('连接本地浏览器', 10); const targetUrl = 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1'; const prepared = await preparePage(chromium, { targetUrl }); browser = prepared.browser; @@ -144,6 +149,7 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation await page.waitForTimeout(4000 + Math.random() * 300); + reportProgress('定位目标页面', 30); await ensureRoomPriceCalendar(page); const roomType = process.env.ROOM_TYPE; @@ -153,12 +159,14 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation if (!roomType || !startDate) { log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.'); + reportProgress('执行完成', 100); return; } const container = page.locator('#price-reserve-table-container'); await container.waitFor({ state: 'visible' }); + reportProgress('操作房态数据', 60); const dateList = buildDateList(startDate, endDate); for (const mmdd of dateList) { const dateIndex = await findHeaderDateIndex(container, mmdd); @@ -167,12 +175,15 @@ const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation } await toggleRoomByDateIndex(container, { roomType, dateIndex, operation }); await page.waitForTimeout(600 + Math.random() * 600); - } + } + + reportProgress('保存并校验', 90); } catch (error) { log.error(error); process.exitCode = 1; } finally { await safeDisconnectBrowser(browser); + reportProgress('执行完成', 100); process.exit(process.exitCode || 0); } })(); diff --git a/electron/scripts/mt_trace.js b/electron/scripts/mt_trace.js index 2b8a223..22e4a27 100644 --- a/electron/scripts/mt_trace.js +++ b/electron/scripts/mt_trace.js @@ -4,6 +4,10 @@ import tabsPkg from './common/tabs.js'; const { preparePage, safeDisconnectBrowser } = tabsPkg; +function reportProgress(step, percent) { + console.log('__ZN_PROGRESS__' + JSON.stringify({ step, percent })); +} + const parseDateInput = (dateStr) => { if (!dateStr) return null; if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr); @@ -173,6 +177,7 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => { let browser; try { + reportProgress('连接本地浏览器', 10); const targetUrl = 'https://me.meituan.com/ebooking/merchant/product#/index'; const prepared = await preparePage(chromium, { targetUrl }); browser = prepared.browser; @@ -180,6 +185,7 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => { await page.waitForTimeout(4000 + Math.random() * 300); + reportProgress('定位目标页面', 30); // Navigation logic from user snippet try { const menu = page.locator('#vina-menu-item-100114001 > .lz-submenu-title'); @@ -194,7 +200,7 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => { await page.waitForTimeout(1000); } } - + const calendarLink = page.locator('a').filter({ hasText: '房价房量日历' }); if (await calendarLink.isVisible()) { await calendarLink.click(); @@ -211,12 +217,14 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => { if (!roomType || !startDate) { log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.'); + reportProgress('执行完成', 100); return; } // Wait for table await page.locator('.vxe-table--body-wrapper').waitFor({ state: 'visible', timeout: 15000 }); + reportProgress('操作房态数据', 60); const dateList = buildDateList(startDate, endDate); for (const mmdd of dateList) { try { @@ -227,11 +235,13 @@ const toggleRoom = async (page, { roomType, mmdd, operation }) => { await page.waitForTimeout(600 + Math.random() * 600); } + reportProgress('保存并校验', 90); } catch (error) { log.error(error); process.exitCode = 1; } finally { await safeDisconnectBrowser(browser); + reportProgress('执行完成', 100); process.exit(process.exitCode || 0); } })(); diff --git a/electron/service/config-service/index.ts b/electron/service/config-service/index.ts index a4f61d9..1f727f6 100644 --- a/electron/service/config-service/index.ts +++ b/electron/service/config-service/index.ts @@ -15,6 +15,7 @@ const DEFAULT_CONFIG: IConfig = { [CONFIG_KEYS.DEFAULT_MODEL]: null, [CONFIG_KEYS.SELECTED_CHANNELS]: [], [CONFIG_KEYS.IMAGE_CACHE]: [], + [CONFIG_KEYS.TASK_LIST]: [], } export class ConfigService { diff --git a/electron/service/execute-script-service/index.ts b/electron/service/execute-script-service/index.ts index 553ed77..a3bc636 100644 --- a/electron/service/execute-script-service/index.ts +++ b/electron/service/execute-script-service/index.ts @@ -8,6 +8,8 @@ export class executeScriptService extends EventEmitter { async executeScript( scriptPath: string, options: Record, + taskId?: string, + subTaskId?: string, ): Promise<{ success: boolean; exitCode: number | null; stdoutTail: string; stderrTail: string; error?: string }> { const MAX_TAIL = 32 * 1024; @@ -48,6 +50,20 @@ export class executeScriptService extends EventEmitter { const text = data.toString(); stdoutTail = appendTail(stdoutTail, text); log.info(`stdout: ${text}`); + + if (text.includes('__ZN_PROGRESS__')) { + try { + const jsonStr = text.split('__ZN_PROGRESS__')[1]?.trim(); + if (jsonStr) { + const parsed = JSON.parse(jsonStr); + this.emit('progress', { taskId, subTaskId, ...parsed }); + } + } catch { + // ignore invalid JSON + } + } + + this.emit('stdout', { taskId, subTaskId, text }); }); } @@ -56,6 +72,7 @@ export class executeScriptService extends EventEmitter { const text = data.toString(); stderrTail = appendTail(stderrTail, text); log.info(`stderr: ${text}`); + this.emit('stderr', { taskId, subTaskId, text }); }); } diff --git a/global.d.ts b/global.d.ts index 6585fce..2d1751a 100644 --- a/global.d.ts +++ b/global.d.ts @@ -48,6 +48,11 @@ declare global { return: void } + // 任务事件 + [IPC_EVENTS.TASK_PROGRESS]: { params: [payload: any]; return: void; } + [IPC_EVENTS.TASK_STARTED]: { params: [payload: any]; return: void; } + [IPC_EVENTS.TASK_COMPLETED]: { params: [payload: any]; return: void; } + // 主题事件 [IPC_EVENTS.THEME_MODE_UPDATED]: { params: [isDark: boolean] @@ -157,6 +162,10 @@ declare global { }, // 执行脚本 executeScript: (options: any) => Promise<{success: boolean, error?: string}>, + // 任务事件 + onTaskProgress: (cb: (payload: any) => void) => () => void; + onTaskStarted: (cb: (payload: any) => void) => () => void; + onTaskCompleted: (cb: (payload: any) => void) => () => void; // 打开渠道 openChannel: (channels: any) => Promise<{success: boolean, error?: string}>, // 脚本管理 diff --git a/src/components/TaskList/Card.vue b/src/components/TaskList/Card.vue deleted file mode 100644 index 947836f..0000000 --- a/src/components/TaskList/Card.vue +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/components/TaskList/index.vue b/src/components/TaskList/index.vue deleted file mode 100644 index 25f7158..0000000 --- a/src/components/TaskList/index.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/lib/constants.ts b/src/lib/constants.ts index ab0402d..321409f 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -52,6 +52,11 @@ export enum IPC_EVENTS { // 执行脚本 EXECUTE_SCRIPT = 'execute-script', + // 任务事件 + TASK_PROGRESS = 'task:progress', + TASK_STARTED = 'task:started', + TASK_COMPLETED = 'task:completed', + // 打开渠道 OPEN_CHANNEL = 'open-channel', @@ -104,6 +109,7 @@ export enum CONFIG_KEYS { AUTO_DOWNLOAD_UPDATE = 'autoDownloadUpdate', SELECTED_CHANNELS = 'selectedChannels', IMAGE_CACHE = 'imageCache', + TASK_LIST = 'taskList', } export enum MENU_IDS { diff --git a/src/lib/task-types.ts b/src/lib/task-types.ts new file mode 100644 index 0000000..f029bc3 --- /dev/null +++ b/src/lib/task-types.ts @@ -0,0 +1,40 @@ +export type SubTaskStatus = 'pending' | 'running' | 'success' | 'failed'; + +export interface SubTask { + id: string; + taskId: string; + scriptId: string; + name: string; + status: SubTaskStatus; + progress: number; + message: string; + stdoutTail: string; + stderrTail: string; + error?: string; + startedAt: string; + completedAt?: string; +} + +export type TaskStatus = 'pending' | 'running' | 'success' | 'partial_failed' | 'failed'; + +export interface Task { + id: string; + title: string; + operation: 'open' | 'close'; + roomType: string; + dateRange: [string, string]; + status: TaskStatus; + subTasks: SubTask[]; + roomList: any[]; + createdAt: string; + updatedAt: string; +} + +export interface TaskProgressPayload { + taskId: string; + subTaskId: string; + progress?: number; + message?: string; + stdoutTail?: string; + stderrTail?: string; +} diff --git a/src/pages/home/components/Task.vue b/src/pages/home/components/Task.vue deleted file mode 100644 index ebb19dc..0000000 --- a/src/pages/home/components/Task.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/pages/home/components/TaskCard.vue b/src/pages/home/components/TaskCard.vue new file mode 100644 index 0000000..773d633 --- /dev/null +++ b/src/pages/home/components/TaskCard.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/src/pages/home/components/TaskList.vue b/src/pages/home/components/TaskList.vue new file mode 100644 index 0000000..835df82 --- /dev/null +++ b/src/pages/home/components/TaskList.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/src/pages/home/components/TaskOperationDialog.vue b/src/pages/home/components/TaskOperationDialog.vue index 3af0794..bc57452 100644 --- a/src/pages/home/components/TaskOperationDialog.vue +++ b/src/pages/home/components/TaskOperationDialog.vue @@ -41,8 +41,11 @@