flowauto
Version:
Google Chrome Automation Executor
1,397 lines (1,233 loc) • 41.3 kB
JavaScript
const puppeteer = require('puppeteer');
const path = require('path')
const fs = require('fs');
const { exec } = require('child_process');
const FeishuBaseTable = require('./feishu.base.table')
const os = process.platform;
console.log('os---', os)
// const clipboardy = require('clipboardy');
const { keyboard, Key, sleep, mouse, Button, Point } = require('@nut-tree-fork/nut-js');
// const Clipboard = require('@nut-tree-fork/default-clipboard-provider');
const version = '0.1.1'
const waitForTimeout = (waitTime) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, waitTime)
})
}
let argv = require('minimist')(process.argv.slice(2), {
// string: ['filepath'],
// string: ['userDataDir']
});
const winston = require('winston');
const RunEnv = require('./run.evn');
const kill = require('./kill');
const TaskExecutionCallback = require('./TaskExecutionCallback');
// 创建全局 callback 实例
const callback = new TaskExecutionCallback()
// 封装执行命令的 async 函数
async function executeCommand(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`Error: ${error.message}`);
return;
}
if (stderr) {
reject(`stderr: ${stderr}`);
return;
}
resolve(stdout);
});
});
}
let logFilename = ""
// 创建一个 winston 日志记录器
let logger = {
info: (message) => {
console.log(message)
},
error: (message) => {
console.error(message)
}
}
// const TaskData = require('./task.data')
const ENV = 'mac'
const DEV_CONFIG = {
win: '',
mac: ''
}
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
// 统一的日志函数
const logWithCallback = {
info: (message, ...args) => {
callback.onLog('info', message, ...args)
if (logger && logger.info) {
logger.info(message, ...args)
} else {
console.log(`INFO: ${message}`, ...args)
}
},
error: (message, ...args) => {
callback.onLog('error', message, ...args)
if (logger && logger.error) {
logger.error(message, ...args)
} else {
console.error(`ERROR: ${message}`, ...args)
}
},
warn: (message, ...args) => {
callback.onLog('warn', message, ...args)
if (logger && logger.warn) {
logger.warn(message, ...args)
} else {
console.warn(`WARN: ${message}`, ...args)
}
},
log: (message, ...args) => {
callback.onLog('log', message, ...args)
console.log(message, ...args)
},
debug: (message, ...args) => {
callback.onLog('debug', message, ...args)
if (logger && logger.debug) {
logger.debug(message, ...args)
} else {
console.debug(`DEBUG: ${message}`, ...args)
}
}
}
// console.log(TaskData)
const getBrowser = async (headless = false) => {
// const buildCrx = path.join(__dirname, `/chrome_win64/chrome_extension/XPathHelper`)
// const jestProCrx = path.join(__dirname, `/chrome_win64/chrome_extension/JestPro`)
// console.log(buildCrx)
const config = {
headless: headless, // 关闭无头模式
// userDataDir: `/Users/sh/Library/Application Support/Google/Chrome`,
userDataDir: argv.userDataDir,
// ignoreDefaultArgs: ['--disable-extensions'],
// timeout: 0,
args: [
'--disable-extensions',
'--disable-plugins',
'--disable-dev-shm-usage',
'--no-sandbox',
'--start-maximized',
// `--disable-extensions-except=${buildCrx},${jestProCrx}`,
// `--load-extension=${buildCrx},${jestProCrx}`,
],
ignoreHTTPSErrors: true, // 在导航期间忽略 HTTPS 错误
defaultViewport: null,
// args: ['--start-maximized', ], // 最大化启动,开启vue-devtools插件
// defaultViewport: { // 为每个页面设置一个默认视口大小
// width: 820,
// height: 900
// }
}
// if (DEV_CONFIG[ENV]) {
// config.executablePath = path.join(__dirname, DEV_CONFIG[ENV])
// logger.info(config.executablePath)
// }
const browser = await puppeteer.launch(config);
return browser
}
const runNodeStart = async (arg) => {
const { browser, task } = arg
const { url, optsetting } = task
const { handleType = 'web', command = '', waitTime = 0 } = optsetting || {}
if (handleType === 'web') {
const page = await browser.newPage()
logWithCallback.info(`打开页面:${url}`)
await page.goto(url, {
waitUntil: 'domcontentloaded',
});
return { page }
} else {
await executeCommand(command)
await sleep(1000 * waitTime)
}
return {}
// await page.keyboard.press('Tab'); // 通过按下 Tab 键切换焦点
}
const runNodeOpt = async (arg) => {
const { browser, task, page } = arg
const { optsetting } = task
const { optType } = optsetting
if (typeof RUN_OPT_TYPE[optType] === 'function') {
return await RUN_OPT_TYPE[optType]({ ...arg, optsetting })
}
return {}
}
const runNodeEnd = async (arg) => {
const { browser, task, page } = arg
// await page.addScriptTag({ content: `let TASK_END_JEST_LOG = '${logFilename}';` })
// await page.evaluate(() => {
// alert(`任务执行结束,详情可前往查看日志【${TASK_END_JEST_LOG} 】!`)
// });
await browser.close();
logWithCallback.info(`任务结束`)
return {}
}
const runOptClick = async (arg) => {
const { browser, optsetting, page, logicType, frequency, } = arg
let { xpath, waitTime, clickData } = optsetting
const { clickMethod, levelXpath, fixXpath } = clickData
if (logicType === 'loop' && levelXpath && clickMethod === 'list' && frequency !== undefined && frequency !== null) {
xpath = `${levelXpath.replace('$index', frequency)}${fixXpath}`
// console.log('xpath---', xpath)
}
const clickElement = await page.$(`::-p-xpath(${xpath})`, { timeout: 0 })
// console.log('clickElement---', clickElement )
const oldPages = await browser.pages()
await clickElement.focus()
await clickElement.click();
// console.log('click')
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
// await page.waitForNavigation()
const { isCurrentPage } = clickData
logWithCallback.info(`点击 `)
if (isCurrentPage !== 1) {
let newPage = page
const pages = await browser.pages()
newPage = pages[pages.length - 1]
// console.log('pages----', pages, oldPages)
if (oldPages.length < pages.length) {
await newPage.bringToFront()
logWithCallback.info(`切换新页面tab`)
} else {
logWithCallback.info(`切换新页面`)
}
return { page: newPage }
}
return {}
}
const runOptInput = async (arg) => {
const { browser, optsetting, page, env } = arg
const { xpath, waitTime, inputData } = optsetting
const clickElement = await page.$(`::-p-xpath(${xpath})`, { timeout: 0 })
await clickElement.focus()
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
let { inputValue, inputType } = inputData
if (inputType === 'paramType') {
inputValue = env.get(inputValue) || inputValue
}
await clickElement.type(String(inputValue), { delay: 500 })
logWithCallback.info(`输入: ${inputValue} `)
return {}
}
const runOptVerify = async (arg) => {
const { browser, optsetting, page } = arg
const { xpath, waitTime, verifyData } = optsetting
const clickElement = await page.$(`::-p-xpath(${xpath})`, { timeout: 0 })
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
const { verifyValue, rename, tipType } = verifyData
const text = await page.evaluate(node => node.innerText, clickElement)
if (text === verifyValue) {
logWithCallback.log(`${rename}:通过`)
logWithCallback.info(`校验: ${rename} 【通过】`)
return true
} else {
logWithCallback.log(`${rename}:测试不通过`)
logWithCallback.info(`校验: ${rename} 【不通过】`)
if (tipType === 'tip_alert') {
await page.addScriptTag({ content: `const VERIFY_TIP = '${rename}:测试不通过'` })
await page.evaluate(() => {
alert(VERIFY_TIP)
});
}
}
return false
}
const runOptPick = async (arg) => {
const { browser, optsetting, page } = arg
const { pickData } = optsetting
const { pickType } = pickData
if (typeof RUN_PICK_TYPE[pickType] === 'function') {
return await RUN_PICK_TYPE[pickType]({ ...arg, optsetting })
}
return {}
}
const runOptHover = async (arg) => {
const { optsetting, page, } = arg
const { xpath, waitTime, } = optsetting
const clickElement = await page.$(`::-p-xpath(${xpath})`, { timeout: 0 })
await clickElement.hover()
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
return {}
}
const runOptExists = async (arg) => {
const { optsetting, page, logicType, frequency } = arg
let { xpath, waitTime, existsData } = optsetting
const { rename, pickMethod, levelXpath, fixXpath } = existsData
if (logicType === 'loop' && levelXpath && pickMethod === 'list' && frequency !== undefined && frequency !== null) {
xpath = `${levelXpath.replace('$index', frequency)}${fixXpath}`
console.log('xpath---', xpath)
}
try {
await page.$(`::-p-xpath(${xpath})`, { timeout: 0 })
logWithCallback.log(`${rename}元素存在`)
} catch (error) {
logWithCallback.log(`${rename}元素不存在`)
return false
}
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
return true
}
const runPick = async (arg) => {
const { browser, optsetting, page, logicType, frequency, env, } = arg
let { xpath, waitTime, pickData, rename: renamept } = optsetting
const { rename, pickType, pickMethod, levelXpath, fixXpath } = pickData
if (logicType === 'loop' && levelXpath && pickMethod === 'list' && frequency !== undefined && frequency !== null) {
xpath = `${levelXpath.replace('$index', frequency)}${fixXpath}`
// console.log('xpath---', xpath)
}
const clickElement = await page.$(`::-p-xpath(${xpath})`, { timeout: 0 })
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
await page.addScriptTag({
content: `const PICK_VALUE = {
'pick_text': 'innerText',
'pick_src': 'src',
'pick_href': 'href'
};
const pickType = '${pickType}'`,
})
const text = await page.evaluate(node => {
if (node) {
return node[PICK_VALUE[pickType]] || node.innerText
}
return ""
}, clickElement)
console.log(`${renamept || rename}:${text}`)
logWithCallback.info(`采集数据:${renamept || rename}:${text}`)
env && (typeof env.pickData === 'function') && env.pickData({
key: renamept || rename,
value: text,
})
return {}
}
const runNodeLogic = async (arg) => {
const { browser, task, page, } = arg
const { logicsetting, waitTime } = task
const { logicType } = logicsetting
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
// console.log('logicsetting---', logicsetting)
if (typeof RUN_LOGIC[logicType] === 'function') {
return await RUN_LOGIC[logicType]({ ...arg, logicsetting })
}
return {}
}
const runLogicLoop = async (arg) => {
const { logicsetting } = arg
const { loopType } = logicsetting
logWithCallback.info(`执行循环`)
if (typeof LOOP_TYPE[loopType] === 'function') {
return await LOOP_TYPE[loopType]({ ...arg, logicsetting })
}
return {}
}
const runLogicExport = async (arg) => {
const { logicsetting } = arg
const { dataType } = logicsetting
if (typeof EXPORT_DATA_TYPE[dataType] === 'function') {
return await EXPORT_DATA_TYPE[dataType]({ ...arg, logicsetting })
}
return {}
}
const runLogicKeyboard = async (arg) => {
const { optsetting } = arg
const { keyType } = optsetting
if (typeof KEY_BOARD_TYPE_EVENT[keyType] === 'function') {
return await KEY_BOARD_TYPE_EVENT[keyType]({ ...arg })
}
return {}
}
const handKeyInput = async (arg) => {
const { optsetting, env } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
if (inputData.inputType === 'paramType') {
inputValue = env.get(inputValue) || inputValue
}
keyboard.config.autoDelayMs = 0;
// await new Clipboard.default().copy(inputValue)
// await sleep(1000);
// await keyboard.pressKey(Key.Comma, Key.V); // 对于 macOS 使用 Key.LeftCommand
// await keyboard.releaseKey(Key.Comma, Key.V); // 对于 macOS 使用 Key.LeftCommand
// await new Clipboard.default().paste(inputValue)
await keyboard.type(inputValue);
await sleep(waitTime * 1000);
// console.log('text---', text)
logWithCallback.info(`执行输入操作`)
return {}
}
const handKeyTab = async (arg) => {
const { optsetting } = arg
const { waitTime } = optsetting
// await keyboard.pressKey(Key.Tab);
await keyboard.type(Key.Tab);
await sleep(waitTime * 1000);
logWithCallback.info(`执行Tab操作`)
return {}
}
const handKeyEnter = async (arg) => {
const { optsetting } = arg
const { waitTime } = optsetting
keyboard.config.autoDelayMs = 50;
await keyboard.type(Key.Enter);
await sleep(waitTime * 1000);
logWithCallback.info(`执行Enter操作`)
return {}
}
const handKeyEsc = async (arg) => {
const { optsetting } = arg
const { waitTime } = optsetting
// 配置自动延迟
keyboard.config.autoDelayMs = 50;
// 模拟按下 Esc 键
await keyboard.type(Key.Escape);
await sleep(waitTime * 1000);
logWithCallback.info(`执行Esc操作`)
return {}
}
const handKeySreach = async (arg) => {
const { optsetting } = arg
const { waitTime } = optsetting
keyboard.config.autoDelayMs = 50;
if (os === 'darwin') {
// macOS: Command + F
await keyboard.pressKey(Key.LeftCmd, Key.F);
await sleep(400);
await keyboard.releaseKey(Key.LeftCmd, Key.F);
// 确保按键有适当延迟
// await keyboard.pressKey(Key.LeftCmd);
// await keyboard.pressKey(Key.F);
// await sleep(400);
// await keyboard.releaseKey(Key.F);
// await keyboard.releaseKey(Key.LeftCmd);
} else if (os === 'win32') {
// Windows: Ctrl + F
await keyboard.pressKey(Key.Control, Key.F);
}
await sleep(waitTime * 1000);
logWithCallback.info(`执行搜索操作`)
return {}
}
const getShortcutKey = (arg) => {
const { optsetting, env } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
if (!inputValue) {
return []
}
return inputValue.split(',').map(item => {
return Number(item)
})
}
const handKeyShortcut = async (arg) => {
const { optsetting } = arg
const { waitTime } = optsetting
keyboard.config.autoDelayMs = 50;
const keys = getShortcutKey(arg)
console.log('keys----', keys, Key.LeftCmd)
if (keys.length) {
// await keyboard.pressKey(...keys)
if (os === 'darwin') {
// macOS: Command + F
// await keyboard.pressKey(Key.Comma, Key.F);
// await keyboard.releaseKey(Key.Comma, Key.F);
await keyboard.pressKey(...keys);
// for (let i = 0; i < keys.length; i++) {
// await keyboard.pressKey(keys[i])
// }
// 确保按键有适当延迟
// await keyboard.pressKey(Key.LeftCmd);
// await keyboard.pressKey(Key.F);
await sleep(400);
await keyboard.releaseKey(...keys);
// for (let i = 0; i < keys.length; i++) {
// await keyboard.releaseKey(keys[i])
// }
// await keyboard.releaseKey(Key.F);
// await keyboard.releaseKey(Key.LeftCmd);
} else if (os === 'win32') {
// Windows: Ctrl + F
await keyboard.pressKey(...keys);
}
}
await sleep(waitTime * 1000);
logWithCallback.info(`执行快捷键操作`)
return {}
}
const KEY_BOARD_TYPE_EVENT = {
'enter': handKeyEnter,
'input': handKeyInput,
'tab': handKeyTab,
'esc': handKeyEsc,
'sreach': handKeySreach,
'shortcut': handKeyShortcut,
}
const runLogicBack = async (arg) => {
const { page, logicsetting, browser } = arg
await page.goBack()
const { waitTime } = logicsetting
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
const pages = await browser.pages()
const newPage = pages.length > 0 ? pages[pages.length - 1] : null
return { page: newPage }
}
const runLogicReload = async (arg) => {
const { page, logicsetting } = arg
await page.reload()
const { waitTime } = logicsetting
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
return {}
}
const runLogicClose = async (arg) => {
const { page, logicsetting, browser } = arg
await page.close()
const { waitTime } = logicsetting
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
const pages = await browser.pages()
const newPage = pages.length > 0 ? pages[pages.length - 1] : null
return { page: newPage }
}
const runLogicPDF = async (arg) => {
const { logicsetting, env } = arg
const { savaPath, rename } = logicsetting
await page.pdf({
path: path.join(savaPath, `${rename || `page-pdf-${new Date().getTime()}`}.pdf`),
format: 'A4'
});
console.log(`导出成功,pdf路径:${savaPath}`)
logWithCallback.info(`导出成功,pdf路径:${savaPath}`)
return {}
}
const runLogicFunc = async (arg) => {
const { logicsetting, env } = arg
const { selfFuncCode, rename } = logicsetting
logWithCallback.info(`自定义事件: ${rename}`)
if (selfFuncCode) {
let selfFunc = null
try {
selfFunc = AsyncFunction('arg', decodeURIComponent(selfFuncCode))
} catch (error) {
logWithCallback.error(`自定义事件: ${rename}解析错误: ${error}`)
}
if (typeof selfFunc === 'function') {
const res = await selfFunc({...arg, logWithCallback})
try {
logWithCallback.info(`自定义事件: ${rename} 执行结果: ${ JSON.stringify(res) }`)
} catch (error) {
logWithCallback.error(`自定义事件: ${rename} 执行结果解析错误: ${error}`)
}
return res
}
}
return {}
}
const runLogicJSFunc = async (arg) => {
const { logicsetting, page, env } = arg
const { selfFuncCode, rename , waitTime } = logicsetting
logWithCallback.info(`自定义js事件: ${rename}`)
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
if (selfFuncCode) {
await page.addScriptTag({ content: `;var JEST_JS_FUNC = (arg) => { ${decodeURIComponent(selfFuncCode)} }; ` })
const res = await page.evaluate((arg) => {
return JEST_JS_FUNC({...arg})
}, { ...arg });
try {
logWithCallback.info(`自定义js事件: ${rename} 执行结果: ${ JSON.stringify(res) }`)
} catch (error) {
logWithCallback.error(`自定义js事件: ${rename} 执行结果解析错误: ${error}`)
}
return res || {}
}
return {}
}
const runLogicNewPage = async (arg) => {
const { page, logicsetting, browser } = arg
const { waitTime } = logicsetting
logWithCallback.info('获取最新页面')
const pages = await browser.pages()
const newPage = pages.length > 0 ? pages[pages.length - 1] : null
if (waitTime > 0) {
await waitForTimeout(waitTime * 1000)
}
return { page: newPage }
}
const runLogicCondition = async (arg) => {
const { page, logicsetting, } = arg
const { condition, noBody, yesBody } = logicsetting
if (Array.isArray(condition) && condition.length) {
let condStatus = await runTask({ ...arg, taskData: condition, currentPage: page, })
if (typeof condStatus !== 'boolean') {
logWithCallback.log('解析条件件返回类型不是Boolean')
return {}
}
let taskData = []
if (condStatus === true && Array.isArray(yesBody) && yesBody.length) {
taskData = yesBody
} else if (condStatus === false && Array.isArray(noBody) && noBody.length) {
taskData = noBody
}
await runTask({ ...arg, taskData, currentPage: page, })
}
return {}
}
const runLogicList = async (arg) => {
const { page, logicsetting, env } = arg
const { listBody } = logicsetting
if (Array.isArray(listBody) && listBody.length) {
for (let i = 0; i < listBody.length; i++) {
const listEnv = new RunEnv()
await runTask({ ...arg, taskData: [listBody[i]], env: listEnv, logicType: 'list' })
env.pickData(listEnv.getPickData())
}
}
return {}
}
const runLogicListItem = async (arg) => {
const { page, logicsetting, } = arg
const { taskBody } = logicsetting
if (Array.isArray(taskBody) && taskBody.length) {
await runTask({ ...arg, taskData: taskBody, logicType: 'listitem' })
}
return {}
}
const runExportText = async (arg) => {
const { logicsetting } = arg
const { fileType } = logicsetting
if (typeof EXPORT_FILE_TYPE[fileType] === 'function') {
return await EXPORT_FILE_TYPE[fileType]({ ...arg, logicsetting })
}
return {}
}
async function runExportToFeishuExcel(arg) {
const { logicsetting, env, export_data } = arg
const { appToken, personalBaseToken, tableId } = logicsetting
logWithCallback.info(`开始导出飞书多维表格`)
const feishuBaseTable = new FeishuBaseTable(appToken, personalBaseToken, tableId)
await feishuBaseTable.initTable()
let datas = export_data || env.getPickData()
if (!!export_data === false && Array.isArray(datas) && datas.length) {
datas = handleDefault(datas)
}
if (!Array.isArray(datas)) {
throw new Error('导出飞书多维表格必须是数组')
}
// 创建表格字段
const fields = createFields(datas)
logWithCallback.info(`飞书多维表格字段: ${fields}`)
if (Array.isArray(fields) && fields.length) {
await feishuBaseTable.createTableFields(fields)
await waitForTimeout(2000)
try {
let records = datas.map(item => {
return {
fields: {
...item
} ,
}
})
await feishuBaseTable.batchCreateRecord(records)
} catch (error) {
logWithCallback.error(`导出飞书多维表格失败: ${error}`)
}
} else {
logWithCallback.error(`导出飞书多维表格字段失败`)
}
}
function handleDefault(datas) {
let data = []
let item = {}
for (let i = 0; i < datas.length; i++) {
let key = datas[i].key
let value = datas[i].value
item [key] = value
}
data.push(item)
return data
}
function createFields(data) {
// [ { "" : ""}]
if (Array.isArray(data) && data.length) {
return Object.keys(data[0]).map(item => {
return item
})
}
return []
}
const runExportToJson = async (arg) => {
const { logicsetting, env, export_data } = arg
const { savaPath, rename } = logicsetting
try {
const data = export_data || env.getPickData()
// console.log('data----', data)
const newSavaPath = savaPath ? `${savaPath}/${rename || new Date().getTime()}.json` : path.join(__dirname, `${rename || new Date().getTime()}.json`)
fs.writeFileSync(newSavaPath, JSON.stringify(data, null, 4));
console.log(`导出成功,路径:${newSavaPath}`)
logWithCallback.info(`导出成功,路径:${newSavaPath}`)
} catch (error) {
console.error(error);
}
return {}
}
const onRunLoopFrequency = async (arg) => {
const { logicsetting, page, env } = arg
let { loopBody, frequency } = logicsetting
// console.log('onRunLoopFrequency---', arg)
// console.log('page---', await page.title())
if (Array.isArray(loopBody) && loopBody.length && frequency > 0) {
let index = 1
// const child = env.createChild()
let loopEnv = null
while (frequency >= index) {
loopEnv = new RunEnv()
await runTask({ ...arg, taskData: loopBody, currentPage: page, frequency: index, env: loopEnv, logicType: 'loop', })
index = index + 1
env.pickData(loopEnv.getPickData())
// console.log('loopEnv---', loopEnv)
}
// console.log('Frequency Env---', child)
// console.log('env---', env)
}
return {}
}
const onRunLoopByCondiNode = async (arg) => {
const { logicsetting, page, env } = arg
let { loopcondition, loopBody } = logicsetting
if (Array.isArray(loopcondition) && loopcondition.length) {
let loopEnv = null
let loopStatus = await runTask({ ...arg, taskData: loopcondition, currentPage: page, logicType: 'loopcondition', })
if (typeof loopStatus !== 'boolean') {
logWithCallback.log('解析条件件返回类型不是Boolean')
return {}
}
let index = 1
while (loopStatus) {
logWithCallback.log('解析循环自定义事件')
loopEnv = new RunEnv()
await runTask({ ...arg, taskData: loopBody, currentPage: page, env: loopEnv, logicType: 'loop', frequency: index, })
env.pickData(loopEnv.getPickData())
loopStatus = await runTask({ ...arg, taskData: loopcondition, currentPage: page, logicType: 'loopcondition', })
index = index + 1
}
logWithCallback.log('节点条件循环结束')
}
}
const onRunLoopSelfFunc = async (arg) => {
const { logicsetting, page, env } = arg
let { selfFuncCode, loopBody } = logicsetting
if (selfFuncCode) {
let loopCondition = null
try {
loopCondition = AsyncFunction('arg', decodeURIComponent(selfFuncCode))
logWithCallback.info('解析循环自定义事件')
} catch (error) {
logWithCallback.error('循环自定义事件解析错误')
}
if (typeof loopCondition === 'function') {
let loopEnv = null
let loopStatus = await loopCondition({...arg, logWithCallback})
if (typeof loopStatus !== 'boolean') {
logWithCallback.log('解析循环自定义事件返回类型不是Boolean')
return {}
}
while (loopStatus) {
console.log('解析循环自定义事件')
loopEnv = new RunEnv()
await runTask({ ...arg, taskData: loopBody, currentPage: page, env: loopEnv, logicType: 'loop', })
env.pickData(loopEnv.getPickData())
loopStatus = await loopCondition(arg)
}
}
}
return {}
}
const runLogicMouse = async (arg) => {
const { optsetting } = arg
const { mouseType } = optsetting
console.log('screenType---', mouseType)
if (typeof MOUSE_TYPE_EVENT[mouseType] === 'function') {
return await MOUSE_TYPE_EVENT[mouseType](arg)
}
return {}
}
const handSingleWord = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (!inputValue) {
return {}
}
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
console.log('screen.find(textLine(inputValue))---', straightTo(centerOf(screen.find(textLine(inputValue)))))
await mouse.move(straightTo(centerOf(screen.find(textLine(inputValue)))));
}
// const handTextLine = async (arg) => {
// const { optsetting } = arg
// const { screenType } = optsetting
// if (screenType === 'textLine') {
// await runLogicKeyboard(arg)
// }
// }
const handMouseMove = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (!inputValue) {
return {}
}
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
try {
const datas = JSON.parse(inputValue)
for (let i = 0; i < datas.length; i++) {
const data = datas[i]
await mouse.move(new Point(data[0], data[1]));
}
} catch (error) {
console.log('mouse.move error---', error)
}
return {}
}
const handMouseLeftClick = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
await mouse.click(Button.LEFT);
return {}
}
const handMouseDoubleLeftClick = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
await mouse.doubleClick(Button.LEFT);
return {}
}
const handMouseRightClick = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
await mouse.click(Button.RIGHT);
return {}
}
const handMouseDoubleRightClick = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
await mouse.doubleClick(Button.RIGHT);
return {}
}
const handScrollDown = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (!inputValue) {
return {}
}
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
await mouse.scrollDown(Number(inputValue));
return {}
}
const handScrollUp = async (arg) => {
const { optsetting } = arg
const { inputData, waitTime } = optsetting
let { inputValue } = inputData
console.log('inputValue---', inputValue)
if (!inputValue) {
return {}
}
if (waitTime > 0) {
await sleep(waitTime * 1000)
}
await mouse.scrollUp(Number(inputValue));
}
const MOUSE_TYPE_EVENT = {
// 'singleWord': handSingleWord,
// 'textLine': handTextLine,
'mousemove': handMouseMove,
'mouseleftclick': handMouseLeftClick,
'mousedoubleleftclick': handMouseDoubleLeftClick,
'mouserightclick': handMouseRightClick,
'mousedoublerightclick': handMouseDoubleRightClick,
'scrollDown': handScrollDown,
'scrollUp': handScrollUp,
}
const RUN_NODE_TYPE = {
'start': runNodeStart,
'opt': runNodeOpt,
'logic': runNodeLogic,
'end': runNodeEnd,
}
const RUN_OPT_TYPE = {
'opt_click': runOptClick,
'opt_input': runOptInput,
'opt_verify': runOptVerify,
'opt_pick': runOptPick,
'opt_hover': runOptHover,
'opt_exists': runOptExists,
'opt_keyboard': runLogicKeyboard,
'opt_mouse': runLogicMouse,
}
const RUN_LOGIC = {
'logic_loop': runLogicLoop,
'logic_export': runLogicExport,
'logic_back': runLogicBack,
'logic_reload': runLogicReload,
'logic_close': runLogicClose,
'logic_pdf': runLogicPDF,
'logic_func': runLogicFunc,
'logic_js_func': runLogicJSFunc,
'logic_new_page': runLogicNewPage,
'logic_condition': runLogicCondition,
'logic_list': runLogicList,
'logic_listitem': runLogicListItem,
}
const RUN_PICK_TYPE = {
'pick_text': runPick,
'pick_src': runPick,
'pick_href': runPick
}
const LOOP_TYPE = {
'frequency': onRunLoopFrequency,
'selffunc': onRunLoopSelfFunc,
'cooditionnode': onRunLoopByCondiNode,
}
const EXPORT_DATA_TYPE = {
'text': runExportText
}
const EXPORT_FILE_TYPE = {
'json': runExportToJson,
'feishu_excel': runExportToFeishuExcel,
}
async function runTask(arg) {
let { browser, taskData, currentPage, logicType, env } = arg
let step = 0
let maxStep = taskData.length
let resultData = {}
// console.log('maxStep---', maxStep)
while (step < maxStep && taskData[step]) {
// console.log(step)
const { nodeType } = taskData[step]
const params = {
...arg,
...resultData,
browser,
task: taskData[step],
}
const currentTask = {
...taskData[step],
TASK_RUN_ENV: env.toSerializable(),
TASK_RUN_PARAMS: RunEnv.serialize(params),
}
try {
if (typeof RUN_NODE_TYPE[nodeType] === 'function') {
// 触发步骤开始回调
callback.onStepStart(step, currentTask)
if (currentPage) params.page = currentPage;
resultData = await RUN_NODE_TYPE[nodeType](params);
if (typeof resultData === 'object' && Object.keys(resultData).length) {
const { page } = resultData
if (page) currentPage = page;
}
// 触发步骤成功回调
callback.onStepSuccess(step, currentTask, resultData)
} else {
callback.onStepError(step, currentTask, `${nodeType} 不存在 事件`)
}
} catch (error) {
// 触发步骤错误回调
callback.onStepError(step, currentTask, error)
// 重新抛出错误以保持原有的错误处理逻辑
throw error
}
step = step + 1
}
// if (!logicType && currentPage) {
// await currentPage.addScriptTag({ content: `let TASK_END_JEST_LOG = '${logFilename}';` })
// // console.log('logicType---', logicType)
// await currentPage.evaluate(() => {
// alert(`任务执行结束,详情可前往查看日志【${TASK_END_JEST_LOG} 】!`)
// });
// }
// 触发任务完成回调
callback.onComplete(resultData)
return resultData
}
async function main() {
if (argv.version) {
console.log(`success v:${version}`)
return
}
console.log('task run-----',)
if (!argv.userDataDir && argv.mode !== 'dev') {
logger.error('浏览器缓存数据文件夹配置路径不能为空',)
const error = new Error('读浏览器缓存数据文件夹配置路径不能为空');
callback.onError(error);
return
}
await kill()
const env = new RunEnv()
let browser = null
let taskData = []
// console.log('argv----', argv)
// const mode = argv.mode || 'dev'
logger.info(`参数: ${JSON.stringify(argv)}`)
try {
const data = fs.readFileSync(argv.filepath, 'utf-8');
const dataconfig = JSON.parse(data);
taskData = JSON.parse(dataconfig.task)
try {
if (argv.taskparamfile) {
const data = fs.readFileSync(argv.taskparamfile, 'utf-8');
const taskparam = JSON.parse(data || '{}')
console.log('taskparamfile ---', taskparam)
for (let key in taskparam) {
env.set(key, taskparam[key])
}
}
} catch (error) {
console.log('taskparamfile---', error)
callback.onError(error);
}
try {
const taskparam = JSON.parse(argv.taskparam || '{}')
console.log('taskparam---', taskparam)
for (let key in taskparam) {
env.set(key, taskparam[key])
}
} catch (error) {
console.log('taskparam---', error)
callback.onError(error);
}
let headless = false
try {
headless = Boolean(argv.headless) || false
} catch{}
if (Array.isArray(taskData)) {
callback.onStart(taskData)
if (taskData[0].nodeType === 'start'
&& (!taskData[0].optsetting || taskData[0].optsetting.handleType === 'web')) {
browser = await getBrowser(headless)
}
// 加入参数
for (let key in argv) {
env.set(key, argv[key])
}
// console.log('taskData---', taskData)
await runTask({ browser, taskData, env })
logger.info('任务结束',)
} else {
// console.error('读取或解析任务数据时发生错误: 数据类型错误',);
logger.error('读取或解析任务数据时发生错误: 数据类型错误',)
const error = new Error('读取或解析任务数据时发生错误: 数据类型错误');
callback.onError(error);
}
} catch (err) {
// console.error('读取或解析任务数据时发生错误 err:', err);
logger.error('程序执行异常 xxxx:', err)
const error = new Error(`程序执行异常: ${err}`);
callback.onError(error);
}
}
// main()
// ... existing code ...
// 修改 main 函数,添加参数并返回结果
async function execute(options = {}) {
try {
argv = { ...options }
if (options.logpath) {
logDirectory = options.logpath
} else {
let logDirectory = path.join(__dirname, '/tasks-run-logs');
// 确保目录存在
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory, { recursive: true });
}
}
// 创建一个时间戳文件名
const timestamp = new Date().toISOString().replace(/[-:.]/g, '');
logFilename = path.join(logDirectory, `/jest-run-${timestamp}.log`);
callback.removeAllCallback()
// 如果用户提供了回调函数,添加到回调管理器
if (options.onProgress && typeof options.onProgress === 'function') {
callback.addCallback(options.onProgress)
}
// 创建一个 winston 日志记录器
logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
return `${timestamp} ${level.toUpperCase()}: ${message}`;
})
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: logFilename }),
],
});
// 合并传入的选项与命令行参数
const runOptions = { ...options };
if (runOptions.version) {
console.log(`success v:${version}`);
return { success: true, version };
}
if (!runOptions.userDataDir && runOptions.mode !== 'dev') {
logger.error('浏览器缓存数据文件夹配置路径不能为空');
const error = new Error('浏览器缓存数据文件夹配置路径不能为空');
callback.onError(error);
return { success: false, error: '浏览器缓存数据文件夹配置路径不能为空' };
}
await kill();
let browser = {}
const env = new RunEnv();
// 加入参数
for (let key in runOptions) {
env.set(key, runOptions[key]);
}
let taskData = [];
logWithCallback.info(`参数: ${JSON.stringify(runOptions)}`);
const data = fs.readFileSync(runOptions.filepath, 'utf-8');
const dataconfig = JSON.parse(data);
taskData = JSON.parse(dataconfig.task);
if (Array.isArray(taskData)) {
if (taskData[0].nodeType === 'start'
&& (!taskData[0].optsetting || taskData[0].optsetting.handleType === 'web')) {
browser = await getBrowser(runOptions.headless || false)
}
await runTask({ browser, taskData, env });
logger.info('任务结束');
return { success: true };
} else {
logger.error('读取或解析任务数据时发生错误: 数据类型错误');
const error = new Error('读取或解析任务数据时发生错误: 数据类型错误');
callback.onError(error);
return { success: false, error: '数据类型错误' };
}
} catch (err) {
logger.error('程序执行异常 xxxxx:', err);
callback.onError(err);
return { success: false, error: err.message };
}
}
// 仅在直接运行脚本时执行 main()
if (require.main === module) {
callback.removeAllCallback()
// 添加默认回调 - 记录到日志,包含taskdata信息
callback.addCallback((event) => {
switch (event.eventType) {
case 'start':
logger.info(`任务开始执行,共 ${event.totalSteps} 个步骤`)
logger.info(`任务数据概览: ${JSON.stringify(event.taskSummary, null, 2)}`)
break
case 'step_start':
logger.info(`步骤 ${event.step}/${event.totalSteps} 开始: ${event.taskName} (${event.nodeType})`)
logger.info(`当前任务详情: ${JSON.stringify(event.currentTask, null, 2)}`)
if (event.remainingTasks.length > 0) {
logger.info(`剩余 ${event.remainingTasks.length} 个任务待执行`)
}
break
case 'step_success':
logger.info(`步骤 ${event.step} 完成: ${event.taskName} - 耗时 ${event.duration}ms`)
logger.info(`执行结果: ${JSON.stringify(event.result, null, 2)}`)
logger.info(`进度: ${event.step}/${event.totalSteps} (${Math.round(event.step / event.totalSteps * 100)}%)`)
break
case 'step_error':
logger.error(`步骤 ${event.step} 失败: ${event.taskName} - ${event.error}`)
logger.error(`失败任务详情: ${JSON.stringify(event.failedTask, null, 2)}`)
break
case 'complete':
logger.info(`任务执行完成,总耗时 ${event.duration}ms`)
logger.info(`完成任务数: ${event.totalSteps}`)
break
case 'error':
logger.error(`任务执行失败: ${event.error}`)
if (event.failedTaskData) {
logger.error(`失败时的任务数据: ${JSON.stringify(event.failedTaskData, null, 2)}`)
}
break
}
})
// 创建一个时间戳文件名
const timestamp = new Date().toISOString().replace(/[-:.]/g, '');
let logDirectory = ""
console.log("logDirectory ::::", logDirectory)
if (argv.logpath) {
logDirectory = argv.logpath
} else {
let logDirectory = path.join(__dirname, '/tasks-run-logs');
// 确保目录存在
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory, { recursive: true });
}
}
logFilename = path.join(logDirectory, `/jest-run-${timestamp}.log`);
// 创建一个 winston 日志记录器
logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
return `${timestamp} ${level.toUpperCase()}: ${message}`;
})
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: logFilename }),
],
});
main();
}
// 导出 main 函数
module.exports = execute;