feat: 新增任务操作功能

This commit is contained in:
DEV_DSW
2026-03-02 16:55:16 +08:00
parent b701c419d1
commit d99f1dd98e
16 changed files with 294 additions and 17 deletions

2
global.d.ts vendored
View File

@@ -80,6 +80,8 @@ declare global {
warn: (message: string, ...meta?: any[]) => void; warn: (message: string, ...meta?: any[]) => void;
error: (message: string, ...meta?: any[]) => void; error: (message: string, ...meta?: any[]) => void;
}, },
// 任务操作
taskOperation: (params: any) => Promise<{success: boolean, error?: string}>,
} }
interface Window { interface Window {

7
package-lock.json generated
View File

@@ -32,6 +32,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"mitt": "^3.0.1",
"openai": "^6.14.0", "openai": "^6.14.0",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"uuid": "^13.0.0", "uuid": "^13.0.0",
@@ -8640,6 +8641,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",

View File

@@ -69,6 +69,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"mitt": "^3.0.1",
"openai": "^6.14.0", "openai": "^6.14.0",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"uuid": "^13.0.0", "uuid": "^13.0.0",

View File

@@ -52,6 +52,9 @@ export enum IPC_EVENTS {
GET_THEME_MODE = 'get-theme-mode', GET_THEME_MODE = 'get-theme-mode',
IS_DARK_THEME = 'is-dark-theme', IS_DARK_THEME = 'is-dark-theme',
THEME_MODE_UPDATED = 'theme-mode-updated', THEME_MODE_UPDATED = 'theme-mode-updated',
// 任务操作
TASK_OPERATION = 'task-operation',
} }
export const MAIN_WIN_SIZE = { export const MAIN_WIN_SIZE = {

View File

@@ -4,6 +4,7 @@ import { setupMainWindow } from './wins';
import started from 'electron-squirrel-startup' import started from 'electron-squirrel-startup'
import configManager from '@main/service/config-service' import configManager from '@main/service/config-service'
import logManager from '@main/service/logger' import logManager from '@main/service/logger'
import { runTaskOperationService } from '@main/process/runTaskOperationService'
// Handle creating/removing shortcuts on Windows when installing/uninstalling. // Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) { if (started) {
@@ -20,6 +21,11 @@ process.on('unhandledRejection', (reason, promise) => {
app.whenReady().then(() => { app.whenReady().then(() => {
setupMainWindow(); setupMainWindow();
// 开启任务操作子进程
runTaskOperationService()
// 开启subagent子进程
}); });
// Quit when all windows are closed, except on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common

View File

View File

@@ -0,0 +1,2 @@
export function runTaskOperationService() {}

View File

@@ -0,0 +1,7 @@
import { ipcMain } from 'electron'
import { IPC_EVENTS } from '@common/constants'
import { spawn } from 'child_process'
export function runTaskOperationService() {}

View File

@@ -54,7 +54,10 @@ const api: WindowApi = {
info: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta), info: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta),
warn: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta), warn: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta),
error: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta), error: (message: string, ...meta: any[]) => ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta),
} },
// 任务操作
taskOperation: (params: any) => ipcRenderer.invoke(IPC_EVENTS.TASK_OPERATION, params),
} }
contextBridge.exposeInMainWorld('api', api) contextBridge.exposeInMainWorld('api', api)

View File

@@ -4,7 +4,8 @@ export interface taskCenterItem {
title: string title: string
desc: string, desc: string,
id: string, id: string,
icon: string icon: string,
type: 'sale' | 'close' | 'open'
} }
export const taskCenterList: taskCenterItem[] = [ export const taskCenterList: taskCenterItem[] = [
@@ -12,18 +13,21 @@ export const taskCenterList: taskCenterItem[] = [
title: '每日销售数据', title: '每日销售数据',
desc: '分析用于销售渠道每日数据汇总及简要展示', desc: '分析用于销售渠道每日数据汇总及简要展示',
id: uuidv4(), id: uuidv4(),
icon: '销' icon: '销',
type: 'sale'
}, },
{ {
title: '关渠道房型', title: '关渠道房型',
desc: '关闭销售渠道下的指定房型', desc: '关闭销售渠道下的指定房型',
id: uuidv4(), id: uuidv4(),
icon: '关' icon: '关',
type: 'close'
}, },
{ {
title: '开渠道房型', title: '开渠道房型',
desc: '开启销售渠道下的指定房型', desc: '开启销售渠道下的指定房型',
id: uuidv4(), id: uuidv4(),
icon: '开' icon: '开',
type: 'open'
}, },
] ]

View File

@@ -0,0 +1,7 @@
import mitt from 'mitt'
const emitter = mitt();
(window as any).emitter = emitter
export default emitter

View File

@@ -69,8 +69,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, defineEmits, watch, nextTick } from 'vue' import { onMounted, onUnmounted, ref, watch, nextTick } from "vue";
import { onMounted, onUnmounted } from "vue";
import { WebSocketManager } from "@common/WebSocketManager"; import { WebSocketManager } from "@common/WebSocketManager";
import { MessageRole, ChatMessage } from "./model/ChatModel"; import { MessageRole, ChatMessage } from "./model/ChatModel";
import { IdUtils } from "@common/index"; import { IdUtils } from "@common/index";

View File

@@ -2,14 +2,14 @@
<div class="flex-1 pb-6"> <div class="flex-1 pb-6">
<div class="flex justify-between items-center py-4"> <div class="flex justify-between items-center py-4">
<h3 class="text-base font-semibold">任务中心</h3> <h3 class="text-base font-semibold">任务中心</h3>
<a class="text-[#3b82f6] text-[13px] cursor-pointer"> <!-- <a class="text-[#3b82f6] text-[13px] cursor-pointer">
编辑 编辑
</a> </a> -->
</div> </div>
<div class="grid grid-cols-2 gap-4 max-[800px]:grid-cols-1"> <div class="grid grid-cols-2 gap-4 max-[800px]:grid-cols-1">
<div v-for="item in taskList" :key="item.id" class="flex gap-3 items-start p-3.5 <div v-for="item in taskList" :key="item.id" class="flex gap-3 items-start p-3.5
rounded-[10px] border border-[#dfeaf6] bg-white cursor-pointer"> rounded-[10px] border border-[#dfeaf6] bg-white cursor-pointer" @click="handleTaskItem(item)">
<div class="w-11 h-11 bg-[#EFF6FF] rounded-lg <div class="w-11 h-11 bg-[#EFF6FF] rounded-lg
border border-dashed border-[#9fc0e8] border border-dashed border-[#9fc0e8]
flex items-center justify-center flex items-center justify-center
@@ -32,7 +32,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { taskCenterList } from '@constant/taskCenterList' import { taskCenterList, taskCenterItem } from '@constant/taskCenterList'
import emitter from '@utils/emitter'
const taskList = computed(() => taskCenterList) const taskList = computed(() => taskCenterList)
// 点击任务项
const handleTaskItem = (item: taskCenterItem) => {
if (item.type === 'sale') {
return
}
// 操作房型
emitter.emit('OPERATION_CHANNEL', item)
}
</script> </script>

View File

@@ -0,0 +1,79 @@
<template>
<el-dialog v-model="isVisible" :title="title" width="480" align-center>
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="pl-4 pr-4 pt-4">
<el-form-item label="选择房型" prop="roomType">
<el-select v-model="form.roomType" placeholder="请选择房型">
<el-option label="单人间" value="single"></el-option>
<el-option label="双人间" value="double"></el-option>
</el-select>
</el-form-item>
<el-form-item label="选择日期" prop="date">
<el-date-picker v-model="form.date" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="confirm">确认</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { taskCenterItem } from '@constant/taskCenterList'
const isVisible = ref(false)
const title = ref('')
const form = ref({
roomType: '',
date: '',
operation: '',
})
const rules = ref({
roomType: [
{ required: true, message: '请选择房型', trigger: 'blur' },
],
date: [
{ required: true, message: '请选择日期', trigger: 'blur' },
],
})
// 打开弹窗
const open = ({ type }: taskCenterItem) => {
title.value = type === 'open' ? '开启渠道房型' : '关闭渠道房型'
isVisible.value = true
form.value.operation = type
}
// 关闭弹窗
const close = () => {
isVisible.value = false
}
// 重置form
const reset = () => {
form.value.roomType = ''
form.value.date = ''
}
// 取消操作
const cancel = () => {
close()
reset()
}
// 确认操作
const confirm = () => {
close()
console.log(form.value)
window.api.taskOperation(form.value)
}
defineExpose({
open,
close,
})
</script>

View File

@@ -2,28 +2,42 @@
<layout> <layout>
<div class="flex h-full w-full flex-col md:flex-row"> <div class="flex h-full w-full flex-col md:flex-row">
<ChatHistory class="flex-none w-50" @new-chat="guide = true" @select-chat="handleSelectChat" /> <ChatHistory class="flex-none w-50" @new-chat="guide = true" @select-chat="handleSelectChat" />
<div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl"> <div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl">
<ChatBox v-model:guide="guide" :conversationId="selectedConversationId" /> <ChatBox v-model:guide="guide" :conversationId="selectedConversationId" />
</div> </div>
<TaskList /> <TaskList />
</div> </div>
<TaskOperationDialog ref="taskOperationDialogRef" />
</layout> </layout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import TaskList from '@renderer/components/TaskList/index.vue' import TaskList from '@renderer/components/TaskList/index.vue'
import TaskOperationDialog from '@renderer/views/home/components/TaskOperationDialog.vue'
import ChatHistory from './ChatHistory.vue' import ChatHistory from './ChatHistory.vue'
import ChatBox from './ChatBox.vue' import ChatBox from './ChatBox.vue'
import { ref } from 'vue' import emitter from '@utils/emitter'
/// 是否显示引导页
const guide = ref(true)
/// 选择的历史会话ID
const selectedConversationId = ref('')
/// 选择历史会话 // 是否显示引导页
const guide = ref(true)
// 选择的历史会话ID
const selectedConversationId = ref('')
// 任务操作弹窗引用
const taskOperationDialogRef = ref()
// 选择历史会话
const handleSelectChat = (conversationId: string) => { const handleSelectChat = (conversationId: string) => {
guide.value = false; guide.value = false;
selectedConversationId.value = conversationId; selectedConversationId.value = conversationId;
}; };
// 监听任务操作弹窗关闭事件
emitter.on('OPERATION_CHANNEL', (item) => {
taskOperationDialogRef.value?.open(item);
});
</script> </script>

132
src/scripts/fg_trace.js Normal file
View File

@@ -0,0 +1,132 @@
import { chromium } from 'playwright';
import dotenv from 'dotenv';
import log from 'electron-log';
dotenv.config();
const checkLoginStatus = async (page) => {
try {
const currentUrl = await page.url();
log.info('current url==========>:', currentUrl);
const loginPagePatterns = ['/login', '/signin', '/auth'];
const isLoginPage = loginPagePatterns.some(pattern => currentUrl.includes(pattern));
if(!isLoginPage) {
return true;
}
// 检查页面内容中的关键字
const pageContent = await page.content();
const loginKeywords = ['退出', '房价房量管理'];
const hasLoginKeyword = loginKeywords.some(keyword =>
pageContent.includes(keyword)
);
if (hasLoginKeyword) {
return true;
}
return false;
} catch (error) {
return false;
}
}
(async () => {
const browser = await chromium.connectOverCDP('http://localhost:9222');
const context = browser.contexts()[0];
await context.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: ()=> undefined });
});
const pages = await context.pages();
const page = pages.length ? pages[0] : await context.newPage();
await page.goto('https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1');
const isLogin = await checkLoginStatus(page);
if(!isLogin) {
await page.getByRole('textbox', { name: '请输入账号' }).dblclick();
await page.getByRole('textbox', { name: '请输入账号' }).click();
await page.getByRole('textbox', { name: '请输入账号' }).fill(process.env.FZ_USERNAME, { delay: 80 + Math.random() * 120 });
await page.waitForTimeout(1000 + Math.random() * 1000);
await page.getByRole('button', { name: '下一步' }).click();
const frame_1 = await page.frameLocator('#alibaba-login-box');
await page.locator('#alibaba-login-box').contentFrame().getByRole('textbox', { name: '请输入登录密码' }).dblclick();
await page.locator('#alibaba-login-box').contentFrame().getByRole('textbox', { name: '请输入登录密码' }).fill(process.env.FZ_PASSWORD, { delay: 80 + Math.random() * 120 });
await page.waitForTimeout(1000 + Math.random() * 1000);
await page.locator('#alibaba-login-box').contentFrame().getByRole('button', { name: '登录' }).click();
// 等待滑块真正出现在 DOM 并可见
await page.waitForTimeout(4000 + Math.random() * 1000);
const frame_2 = await frame_1.frameLocator('#baxia-dialog-content');
const container = await frame_2.locator('#nc_1_nocaptcha');
const slider = await frame_2.locator('#nc_1_n1z');
const isVisible = await slider.isVisible();
if (isVisible) {
// 重新获取滑块按钮(可能嵌套在 iframe 里)
const containerBox = await container.boundingBox();
const sliderBox = await slider.boundingBox();
const startX = sliderBox.x + sliderBox.width / 2;
const startY = sliderBox.y + sliderBox.height / 2;
const distance = containerBox.width - sliderBox.width; // 适当拉长拖动距离
const steps = 20; // 分多步模拟人手拖动
await page.mouse.move(startX, startY);
// 等待随机时间再开始滑动(模拟人类反应)
await page.waitForTimeout(200 + Math.random() * 300);
await page.mouse.down();
await page.waitForTimeout(100 + Math.random() * 200);
// 按轨迹滑动
for (let i = 0; i < steps; i++) {
await page.mouse.move(
sliderBox.x + sliderBox.width / 2 + (distance * (i + 1) / steps),
sliderBox.y + sliderBox.height / 2 + (Math.random() * 5 - 2.5), // 模拟轻微Y轴抖动
{ steps: 5 }
);
}
await page.mouse.up();
}
}
await page.waitForTimeout(4000 + Math.random() * 300);
await page.getByRole('menuitem', { name: '房价房量管理' }).click();
await page.waitForTimeout(4000 + Math.random() * 1000);
await page.getByText('房价房量日历').click();
// await page.pause();
/*
* 1、我要知道日期
* 2、我要知道房型
* 3、我要知道是关闭或开启操作
*
* 存在以下影响自动化情况:
* 1、房型重新拖拽排序会影响后续操作
* 2、筛选日期下存在没有安排的房型
*/
await page.getByRole('textbox', { name: '-02-27' }).click();
await page.getByText('27').nth(3).click();
// 开启房型div:nth-child(动态变化的) > .boardRow___p2ZiO > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r
await page.locator('div:nth-child(7) > div > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').first().click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(2) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(3) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(4) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(5) > .ant-spin-nested-loading > .ant-spin-container > div > div > .success___VQjXR > .bar___i4k4r').click();
// 关闭房型div:nth-child(动态变化的) > .boardRow___p2ZiO > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').first().click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(2) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(3) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
await page.locator('div:nth-child(7) > .boardRow___p2ZiO > div:nth-child(2) > div:nth-child(4) > .ant-spin-nested-loading > .ant-spin-container > div > div > .error___IM8Yw > .bar___i4k4r').click();
})();