|
@@ -1,6 +1,125 @@
|
|
|
// 动作解析和执行器
|
|
// 动作解析和执行器
|
|
|
// 配置参数
|
|
// 配置参数
|
|
|
const DEFAULT_STEP_INTERVAL = 1000; // 默认步骤间隔1秒
|
|
const DEFAULT_STEP_INTERVAL = 1000; // 默认步骤间隔1秒
|
|
|
|
|
+const DEFAULT_SCROLL_DISTANCE = 100; // 默认每次滚动距离(像素)
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 解析时间字符串(格式:2026/1/13 02:09)
|
|
|
|
|
+ * @param {string} timeStr - 时间字符串
|
|
|
|
|
+ * @returns {Date|null} 解析后的日期对象,失败返回null
|
|
|
|
|
+ */
|
|
|
|
|
+function parseTimeString(timeStr) {
|
|
|
|
|
+ if (!timeStr || timeStr.trim() === '') {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 支持格式:2026/1/13 02:09 或 2026/01/13 02:09
|
|
|
|
|
+ const parts = timeStr.trim().split(' ');
|
|
|
|
|
+ if (parts.length !== 2) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const datePart = parts[0].split('/');
|
|
|
|
|
+ const timePart = parts[1].split(':');
|
|
|
|
|
+
|
|
|
|
|
+ if (datePart.length !== 3 || timePart.length !== 2) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const year = parseInt(datePart[0], 10);
|
|
|
|
|
+ const month = parseInt(datePart[1], 10) - 1; // 月份从0开始
|
|
|
|
|
+ const day = parseInt(datePart[2], 10);
|
|
|
|
|
+ const hour = parseInt(timePart[0], 10);
|
|
|
|
|
+ const minute = parseInt(timePart[1], 10);
|
|
|
|
|
+
|
|
|
|
|
+ const date = new Date(year, month, day, hour, minute, 0, 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证日期是否有效
|
|
|
|
|
+ if (isNaN(date.getTime())) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return date;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('解析时间字符串失败:', error);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 解析延迟字符串(格式:10s, 5m, 2h)
|
|
|
|
|
+ * @param {string} delayStr - 延迟字符串
|
|
|
|
|
+ * @returns {number|null} 延迟的毫秒数,失败返回null
|
|
|
|
|
+ */
|
|
|
|
|
+function parseDelayString(delayStr) {
|
|
|
|
|
+ if (!delayStr || delayStr.trim() === '') {
|
|
|
|
|
+ return 0; // 空字符串表示不延迟
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const trimmed = delayStr.trim();
|
|
|
|
|
+ const unit = trimmed.slice(-1).toLowerCase();
|
|
|
|
|
+ const value = parseInt(trimmed.slice(0, -1), 10);
|
|
|
|
|
+
|
|
|
|
|
+ if (isNaN(value) || value < 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch (unit) {
|
|
|
|
|
+ case 's':
|
|
|
|
|
+ return value * 1000; // 秒转毫秒
|
|
|
|
|
+ case 'm':
|
|
|
|
|
+ return value * 60 * 1000; // 分钟转毫秒
|
|
|
|
|
+ case 'h':
|
|
|
|
|
+ return value * 60 * 60 * 1000; // 小时转毫秒
|
|
|
|
|
+ default:
|
|
|
|
|
+ console.warn(`未知的延迟单位: ${unit},应为 s/m/h`);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('解析延迟字符串失败:', error);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 计算需要等待的时间(毫秒)
|
|
|
|
|
+ * @param {string} data - 执行时间字符串(格式:2026/1/13 02:09)
|
|
|
|
|
+ * @param {string} delay - 延迟字符串(格式:10s, 5m, 2h)
|
|
|
|
|
+ * @returns {number} 需要等待的毫秒数
|
|
|
|
|
+ */
|
|
|
|
|
+function calculateWaitTime(data, delay) {
|
|
|
|
|
+ let targetTime = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有 data 时间,解析它
|
|
|
|
|
+ if (data && data.trim() !== '') {
|
|
|
|
|
+ targetTime = parseTimeString(data);
|
|
|
|
|
+ if (!targetTime) {
|
|
|
|
|
+ console.warn(`无法解析时间字符串: ${data},将立即执行`);
|
|
|
|
|
+ targetTime = new Date(); // 解析失败则立即执行
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 没有 data 时间,使用当前时间
|
|
|
|
|
+ targetTime = new Date();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析 delay
|
|
|
|
|
+ const delayMs = parseDelayString(delay);
|
|
|
|
|
+ if (delayMs === null) {
|
|
|
|
|
+ console.warn(`无法解析延迟字符串: ${delay},将不延迟`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 在目标时间基础上加上延迟
|
|
|
|
|
+ targetTime = new Date(targetTime.getTime() + delayMs);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算需要等待的时间
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const waitTime = targetTime.getTime() - now.getTime();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果目标时间已过,返回0(立即执行)
|
|
|
|
|
+ return Math.max(0, waitTime);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 解析 processing.json 中的操作数组
|
|
* 解析 processing.json 中的操作数组
|
|
@@ -21,11 +140,19 @@ export function parseActions(actions) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 解析公共字段
|
|
|
|
|
+ const times = action.times && action.times > 0 ? parseInt(action.times, 10) : 1;
|
|
|
|
|
+ const data = action.data || '';
|
|
|
|
|
+ const delay = action.delay || '';
|
|
|
|
|
+
|
|
|
// 检查 press 操作
|
|
// 检查 press 操作
|
|
|
if (action.press) {
|
|
if (action.press) {
|
|
|
parsedActions.push({
|
|
parsedActions.push({
|
|
|
type: 'press',
|
|
type: 'press',
|
|
|
value: action.press, // 图片文件名
|
|
value: action.press, // 图片文件名
|
|
|
|
|
+ times: times,
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ delay: delay,
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
// 检查 input 操作
|
|
// 检查 input 操作
|
|
@@ -33,6 +160,9 @@ export function parseActions(actions) {
|
|
|
parsedActions.push({
|
|
parsedActions.push({
|
|
|
type: 'input',
|
|
type: 'input',
|
|
|
value: action.input, // 要输入的文本
|
|
value: action.input, // 要输入的文本
|
|
|
|
|
+ times: times,
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ delay: delay,
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
// 检查 swipe 操作
|
|
// 检查 swipe 操作
|
|
@@ -48,6 +178,9 @@ export function parseActions(actions) {
|
|
|
parsedActions.push({
|
|
parsedActions.push({
|
|
|
type: 'swipe',
|
|
type: 'swipe',
|
|
|
value: swipeValue, // 滑动方向
|
|
value: swipeValue, // 滑动方向
|
|
|
|
|
+ times: times,
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ delay: delay,
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
// 检查 string-press 操作
|
|
// 检查 string-press 操作
|
|
@@ -55,6 +188,27 @@ export function parseActions(actions) {
|
|
|
parsedActions.push({
|
|
parsedActions.push({
|
|
|
type: 'string-press',
|
|
type: 'string-press',
|
|
|
value: action['string-press'], // 要查找的文字
|
|
value: action['string-press'], // 要查找的文字
|
|
|
|
|
+ times: times,
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ delay: delay,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ // 检查 scroll 操作
|
|
|
|
|
+ else if (action.scroll) {
|
|
|
|
|
+ const scrollValue = action.scroll;
|
|
|
|
|
+ const validScrollDirections = ['up-down', 'down-up', 'left-right', 'right-left'];
|
|
|
|
|
+
|
|
|
|
|
+ if (!validScrollDirections.includes(scrollValue)) {
|
|
|
|
|
+ console.warn(`无效的滚动方向: ${scrollValue},应为: ${validScrollDirections.join(', ')}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ parsedActions.push({
|
|
|
|
|
+ type: 'scroll',
|
|
|
|
|
+ value: scrollValue, // 滚动方向
|
|
|
|
|
+ times: times,
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ delay: delay,
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
else {
|
|
else {
|
|
@@ -65,6 +219,34 @@ export function parseActions(actions) {
|
|
|
return parsedActions;
|
|
return parsedActions;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 获取操作名称(用于显示)
|
|
|
|
|
+ * @param {Object} action - 操作对象
|
|
|
|
|
+ * @returns {string} 操作名称
|
|
|
|
|
+ */
|
|
|
|
|
+function getActionName(action) {
|
|
|
|
|
+ const typeNames = {
|
|
|
|
|
+ 'press': '点击图片',
|
|
|
|
|
+ 'input': '输入文本',
|
|
|
|
|
+ 'swipe': '滑动',
|
|
|
|
|
+ 'string-press': '点击文字',
|
|
|
|
|
+ 'scroll': '滚动'
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const typeName = typeNames[action.type] || action.type;
|
|
|
|
|
+ const value = action.value || '';
|
|
|
|
|
+
|
|
|
|
|
+ if (action.type === 'input') {
|
|
|
|
|
+ return `${typeName}: ${value.length > 20 ? value.substring(0, 20) + '...' : value}`;
|
|
|
|
|
+ } else if (action.type === 'string-press') {
|
|
|
|
|
+ return `${typeName}: ${value.length > 20 ? value.substring(0, 20) + '...' : value}`;
|
|
|
|
|
+ } else if (action.type === 'press') {
|
|
|
|
|
+ return `${typeName}: ${value}`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return `${typeName}: ${value}`;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 计算滑动操作的坐标
|
|
* 计算滑动操作的坐标
|
|
|
* @param {string} direction - 滑动方向: up-down, down-up, left-right, right-left
|
|
* @param {string} direction - 滑动方向: up-down, down-up, left-right, right-left
|
|
@@ -115,6 +297,7 @@ export function calculateSwipeCoordinates(direction, width, height) {
|
|
|
return { x1, y1, x2, y2 };
|
|
return { x1, y1, x2, y2 };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 执行单个操作
|
|
* 执行单个操作
|
|
|
* @param {Object} action - 操作对象 {type, value}
|
|
* @param {Object} action - 操作对象 {type, value}
|
|
@@ -224,6 +407,29 @@ export async function executeAction(action, device, folderPath, resolution) {
|
|
|
return { success: true };
|
|
return { success: true };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ case 'scroll': {
|
|
|
|
|
+ // 滚动操作(小幅度滚动)
|
|
|
|
|
+ if (!window.electronAPI || !window.electronAPI.sendScroll) {
|
|
|
|
|
+ return { success: false, error: '滚动 API 不可用' };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const scrollResult = await window.electronAPI.sendScroll(
|
|
|
|
|
+ device,
|
|
|
|
|
+ action.value,
|
|
|
|
|
+ resolution.width,
|
|
|
|
|
+ resolution.height,
|
|
|
|
|
+ DEFAULT_SCROLL_DISTANCE,
|
|
|
|
|
+ 500 // 滚动持续时间500ms,模拟真实手指滑动,避免触发tap事件
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (!scrollResult.success) {
|
|
|
|
|
+ return { success: false, error: `滚动失败: ${scrollResult.error}` };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`成功滚动: ${action.value}`);
|
|
|
|
|
+ return { success: true };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
default:
|
|
default:
|
|
|
return { success: false, error: `未知的操作类型: ${action.type}` };
|
|
return { success: false, error: `未知的操作类型: ${action.type}` };
|
|
|
}
|
|
}
|
|
@@ -263,26 +469,106 @@ export async function executeActionSequence(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const action = actions[i];
|
|
const action = actions[i];
|
|
|
- console.log(`执行步骤 ${i + 1}/${actions.length}: ${action.type} - ${action.value}`);
|
|
|
|
|
|
|
+ const times = action.times || 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 发送步骤开始执行事件
|
|
|
|
|
+ if (onStepComplete) {
|
|
|
|
|
+ const stepName = getActionName(action);
|
|
|
|
|
+ onStepComplete(i + 1, actions.length, stepName, 0, times, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算等待时间(根据 data 和 delay)
|
|
|
|
|
+ const waitTime = calculateWaitTime(action.data, action.delay);
|
|
|
|
|
+
|
|
|
|
|
+ if (waitTime > 0) {
|
|
|
|
|
+ const waitSeconds = Math.round(waitTime / 1000);
|
|
|
|
|
+ console.log(`步骤 ${i + 1}/${actions.length} 等待 ${waitSeconds} 秒后执行...`);
|
|
|
|
|
+
|
|
|
|
|
+ // 在等待期间也更新倒计时
|
|
|
|
|
+ let remainingTime = waitTime;
|
|
|
|
|
+ const countdownInterval = 100;
|
|
|
|
|
+ const stepName = getActionName(action);
|
|
|
|
|
+
|
|
|
|
|
+ while (remainingTime > 0) {
|
|
|
|
|
+ if (shouldStop && shouldStop()) {
|
|
|
|
|
+ return { success: false, error: '执行被停止', completedSteps };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (onStepComplete) {
|
|
|
|
|
+ onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const waitTimeChunk = Math.min(countdownInterval, remainingTime);
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, waitTimeChunk));
|
|
|
|
|
+ remainingTime -= waitTimeChunk;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 根据 times 重复执行操作
|
|
|
|
|
+ for (let t = 0; t < times; t++) {
|
|
|
|
|
+ // 检查是否应该停止
|
|
|
|
|
+ if (shouldStop && shouldStop()) {
|
|
|
|
|
+ console.log('执行被停止');
|
|
|
|
|
+ return { success: false, error: '执行被停止', completedSteps };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 发送步骤执行中事件(包含当前执行次数)
|
|
|
|
|
+ if (onStepComplete) {
|
|
|
|
|
+ const stepName = getActionName(action);
|
|
|
|
|
+ onStepComplete(i + 1, actions.length, stepName, 0, times, t + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (times > 1) {
|
|
|
|
|
+ console.log(`执行步骤 ${i + 1}/${actions.length} (第 ${t + 1}/${times} 次): ${action.type} - ${action.value}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log(`执行步骤 ${i + 1}/${actions.length}: ${action.type} - ${action.value}`);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 执行操作
|
|
|
|
|
- const result = await executeAction(action, device, folderPath, resolution);
|
|
|
|
|
|
|
+ // 执行操作
|
|
|
|
|
+ const result = await executeAction(action, device, folderPath, resolution);
|
|
|
|
|
|
|
|
- if (!result.success) {
|
|
|
|
|
- console.error(`步骤 ${i + 1} 执行失败:`, result.error);
|
|
|
|
|
- return { success: false, error: result.error, completedSteps: i };
|
|
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ console.error(`步骤 ${i + 1} 执行失败:`, result.error);
|
|
|
|
|
+ return { success: false, error: result.error, completedSteps: i };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果不是最后一次重复,等待一小段时间
|
|
|
|
|
+ if (t < times - 1) {
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500)); // 重复执行间隔500ms
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
completedSteps++;
|
|
completedSteps++;
|
|
|
|
|
|
|
|
- // 调用完成回调
|
|
|
|
|
|
|
+ // 调用完成回调,传递步骤信息(当前步骤已完成)
|
|
|
if (onStepComplete) {
|
|
if (onStepComplete) {
|
|
|
- onStepComplete(i + 1, actions.length);
|
|
|
|
|
|
|
+ const stepName = getActionName(action);
|
|
|
|
|
+ onStepComplete(i + 1, actions.length, stepName, 0, times, times);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 如果不是最后一步,等待间隔时间
|
|
|
|
|
|
|
+ // 如果不是最后一步,等待间隔时间并显示倒计时
|
|
|
if (i < actions.length - 1) {
|
|
if (i < actions.length - 1) {
|
|
|
- await new Promise(resolve => setTimeout(resolve, stepInterval));
|
|
|
|
|
|
|
+ // 在等待期间,更新倒计时
|
|
|
|
|
+ let remainingTime = stepInterval;
|
|
|
|
|
+ const countdownInterval = 100; // 每100ms更新一次倒计时
|
|
|
|
|
+ const nextStepName = getActionName(actions[i + 1]);
|
|
|
|
|
+ const nextTimes = actions[i + 1].times || 1;
|
|
|
|
|
+
|
|
|
|
|
+ while (remainingTime > 0) {
|
|
|
|
|
+ // 检查是否应该停止
|
|
|
|
|
+ if (shouldStop && shouldStop()) {
|
|
|
|
|
+ return { success: false, error: '执行被停止', completedSteps };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新倒计时回调(显示下一个步骤的倒计时)
|
|
|
|
|
+ if (onStepComplete) {
|
|
|
|
|
+ onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const waitTime = Math.min(countdownInterval, remainingTime);
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
|
|
|
+ remainingTime -= waitTime;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|