From 667366fdf5869c9a4e579756e9cc2a8423b7b663 Mon Sep 17 00:00:00 2001 From: wangbing Date: Tue, 30 Sep 2025 17:22:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=A4=87=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/xyz/wbsite/jmacro/JMacro.java | 47 ++++++- .../xyz/wbsite/jmacro/JMainController.java | 105 ++++++++++++++- .../java/xyz/wbsite/jmacro/JMainService.java | 50 ++++--- .../java/xyz/wbsite/jmacro/JScheduler.java | 11 +- .../xyz/wbsite/jmacro/util/MousePathUtil.java | 124 ++++++++++++++++++ src/main/java/xyz/wbsite/jtask/TaskImpl.java | 38 +++--- src/main/resources/main.fxml | 42 ++++-- 7 files changed, 361 insertions(+), 56 deletions(-) create mode 100644 src/main/java/xyz/wbsite/jmacro/util/MousePathUtil.java diff --git a/src/main/java/xyz/wbsite/jmacro/JMacro.java b/src/main/java/xyz/wbsite/jmacro/JMacro.java index 66e911a..813e63e 100644 --- a/src/main/java/xyz/wbsite/jmacro/JMacro.java +++ b/src/main/java/xyz/wbsite/jmacro/JMacro.java @@ -1,6 +1,7 @@ package xyz.wbsite.jmacro; import cn.hutool.core.util.RandomUtil; +import org.sikuli.basics.Settings; import org.sikuli.script.FindFailed; import org.sikuli.script.Image; import org.sikuli.script.KeyModifier; @@ -13,8 +14,10 @@ import org.sikuli.script.Screen; import xyz.wbsite.jmacro.base.ColorLocation; import xyz.wbsite.jmacro.base.Legend; import xyz.wbsite.jmacro.util.Logger; +import xyz.wbsite.jmacro.util.MousePathUtil; import xyz.wbsite.jmacro.util.TaskUtil; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -48,6 +51,12 @@ public abstract class JMacro { public JMacro() { this.startFocus(); + // 禁止漂移,所有拟人操作都提前计算好路径,此处的随机要关闭 + Mouse.setRandom(0); + // 鼠标移动延迟(秒),默认值约为 0.5 + Settings.MoveMouseDelay = 0.0f; + // 忽略鼠标移动事件 + Mouse.setMouseMovedAction(0); } public boolean isRun() { @@ -137,7 +146,43 @@ public abstract class JMacro { * @param location 坐标点 */ public void mouseMove(Location location) { - Mouse.move(location); + List path = MousePathUtil.generateBezierPath(Mouse.at(), location); + Settings.MoveMouseDelay = 0.0f; + + // 暂停点索引(-1为不暂停) + int pause = -1; + // 通过概率决断本次是否需要产生暂停 + if (RandomUtil.randomInt(0, 100) < 50) { + int startIdx = path.size() / 3; + int endIdx = 2 * path.size() / 3; + pause = RandomUtil.randomInt(startIdx, endIdx); + } + + for (int[] p : path) { + delay(7 + RandomUtil.randomInt(-3, 3)); + if (pause == path.indexOf(p)) { + Logger.info("拟人操作,停顿片刻", pause); + delay(200 + RandomUtil.randomInt(-100, 200)); + } + Mouse.move(new Location(p[0], p[1])); + } + } + + /** + * 鼠标移动到这个区域 + *

+ * 增加拟人操作,随机移动鼠标到区域中心附近(而非直接移动到中心) + * + * @param region 区域 + */ + public void mouseMove(Region region) { + // 随机移动到区域中心附近(而非直接移动到中心) + Location center = region.getCenter(); + // 得到随机移动到区域中心附近的半径 + int radius = (int) (Math.min(region.getW(), region.getH()) / 2 * 0.9f); + int x = center.getX() + RandomUtil.randomInt(-radius, radius); + int y = center.getY() + RandomUtil.randomInt(-radius, radius); + mouseMove(new Location(x, y)); } /** diff --git a/src/main/java/xyz/wbsite/jmacro/JMainController.java b/src/main/java/xyz/wbsite/jmacro/JMainController.java index 71c320c..101d816 100644 --- a/src/main/java/xyz/wbsite/jmacro/JMainController.java +++ b/src/main/java/xyz/wbsite/jmacro/JMainController.java @@ -7,15 +7,21 @@ import cn.hutool.core.util.StrUtil; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.RadioButton; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; import javafx.util.Duration; import org.sikuli.script.Region; import xyz.wbsite.jmacro.base.Legend; @@ -49,12 +55,18 @@ public class JMainController implements Initializable { @FXML private Button stop; @FXML - private ImageView set; + private ToggleGroup runMode; + @FXML + private HBox modeLoop; + @FXML + private HBox modeTiming; @FXML private TextField interval; @FXML private TextField times; @FXML + private TextField timing; + @FXML private ImageView preview; @FXML private TextArea console; @@ -62,7 +74,7 @@ public class JMainController implements Initializable { private final int MAX_LENGTH = 100; private final BoundedPriorityQueue logs = new BoundedPriorityQueue<>(MAX_LENGTH); - private Semaphore semaphore = new Semaphore(1); + private final Semaphore semaphore = new Semaphore(1); public static synchronized JMainController getInstance() { return JMainController.instance; @@ -71,6 +83,26 @@ public class JMainController implements Initializable { @Override public void initialize(URL location, ResourceBundle resources) { JMainController.instance = this; + + runMode.selectedToggleProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Toggle oldValue, Toggle newValue) { + String mode = ((RadioButton) newValue).getUserData().toString(); + if ("loop".equals(mode)) { + modeLoop.setVisible(true); + modeLoop.setManaged(true); + modeTiming.setVisible(false); + modeTiming.setManaged(false); + } + if ("timing".equals(mode)) { + modeLoop.setVisible(false); + modeLoop.setManaged(false); + modeTiming.setVisible(true); + modeTiming.setManaged(true); + } + JProp.getInstance().setString("mode", mode); + } + }); // 控件初始化 int intervalValue = JProp.getInstance().getInt("interval", 60); this.interval.setText(String.valueOf(intervalValue)); @@ -89,7 +121,60 @@ public class JMainController implements Initializable { } JProp.getInstance().setInt("times", Convert.toInt(this.times.getText())); }); - installTip(this.set, "扩展配置"); + + String timingValue = JProp.getInstance().getString("timing", ""); + this.timing.setText(timingValue); + this.timing.textProperty().addListener((observable, oldValue, newValue) -> { + boolean isValidFormat = true; + String errorMessage = "时间格式错误!支持的格式如:09:30:00,9:35:00,10:45,9:5,8:30"; + + // 空输入视为无效 + if (StrUtil.isEmpty(newValue)) { + isValidFormat = false; + } else { + // 分割多个时间点并逐个验证 + String[] timeParts = newValue.split(","); + for (String part : timeParts) { + String trimmedPart = part.trim(); + // 单个时间点格式验证 (支持 HH:mm:ss、H:mm:ss、HH:mm、H:mm、H:m) + if (!trimmedPart.matches("^\\d{1,2}:\\d{1,2}(?::\\d{1,2})?$")) { + isValidFormat = false; + break; + } + + // 验证时分秒数值范围 + String[] hms = trimmedPart.split(":"); + int hour = Convert.toInt(hms[0], -1); + int minute = Convert.toInt(hms[1], -1); + int second = 0; // 默认秒为0(当没有秒部分时) + + // 处理带秒的格式 + if (hms.length == 3) { + second = Convert.toInt(hms[2], -1); + } + + // 验证数值范围(小时0-23,分钟0-59,秒0-59) + if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) { + isValidFormat = false; + errorMessage = "时间数值错误!时(0-23)、分(0-59)、秒(0-59)"; + break; + } + } + } + + // 设置样式和提示 + if (!isValidFormat) { + timing.setStyle("-fx-text-fill: #ff0000; -fx-border-color: #ff0000; -fx-border-width: 1px;"); + Tooltip errorTooltip = new Tooltip(errorMessage); + errorTooltip.setStyle("-fx-background-color: #fff0f0; -fx-text-fill: #ff0000;"); + Tooltip.install(timing, errorTooltip); + } else { + timing.setStyle("-fx-text-fill: #000000; -fx-border-color: transparent; -fx-border-width: 1px;"); + } + + // 保存配置(修正原代码中的this.times.getText()错误) + JProp.getInstance().setString("timing", this.timing.getText()); + }); installTip(this.interval, "两次脚本执行的间隔时间(秒)"); installTip(this.times, "脚本执行的总次数,0代表无限循环"); } @@ -170,6 +255,7 @@ public class JMainController implements Initializable { synchronized (JMainController.class) { this.start.setDisable(true); this.stop.setDisable(false); + this.saveConfig(); Logger.info("启动服务"); if (!JMainService.getInstance().run) { boolean start = JMainService.start(); @@ -182,6 +268,17 @@ public class JMainController implements Initializable { } } + /** + * 保存配置 + */ + public void saveConfig() { + String string = this.runMode.selectedToggleProperty().getValue().getUserData().toString(); + JProp.getInstance().setString("mode", string); + JProp.getInstance().setInt("times", Convert.toInt(this.times.getText())); + JProp.getInstance().setInt("interval", Convert.toInt(this.interval.getText())); + JProp.getInstance().setString("timing", this.timing.getText()); + } + /** * 停止服务 */ @@ -199,6 +296,8 @@ public class JMainController implements Initializable { } Logger.info("服务停止成功"); this.preview.setImage(null); + } else { + Logger.info("服务未运行"); } } } diff --git a/src/main/java/xyz/wbsite/jmacro/JMainService.java b/src/main/java/xyz/wbsite/jmacro/JMainService.java index 4bfe669..c22ea3b 100644 --- a/src/main/java/xyz/wbsite/jmacro/JMainService.java +++ b/src/main/java/xyz/wbsite/jmacro/JMainService.java @@ -1,12 +1,10 @@ package xyz.wbsite.jmacro; -import cn.hutool.core.collection.CollUtil; import xyz.wbsite.jmacro.base.Legend; import xyz.wbsite.jmacro.util.DialogUtil; import xyz.wbsite.jmacro.util.Logger; import java.io.File; -import java.util.List; /** * 服务线程 @@ -30,25 +28,20 @@ public class JMainService { } /** - * 服务运行状态 + * 任务调度器 */ - public boolean run; + private static JScheduler scheduler = new JScheduler(); /** - * 守护线程 + * 服务运行状态 */ - public MacroTask macroTask = new MacroTask(); + public boolean run; /** * 脚本 */ private JMacro macro; - /** - * 执行次数 - */ - private int count; - /** * 服务内部构造器 */ @@ -67,19 +60,39 @@ public class JMainService { Legend.setDefaultBase(legendDir); } + public MacroTask createTask() { + return new MacroTask(); + } + public static boolean start() { - if (JMainService.getInstance().macro.getWorkRegion() == null) { - JMainService.getInstance().macro.startFocus(); + JMainService service = JMainService.getInstance(); + if (service.macro.getWorkRegion() == null) { + service.macro.startFocus(); DialogUtil.alert("未聚焦到视口,请稍后再试!"); return false; } - if (JMainService.getInstance().run) { + if (service.run) { Logger.info("服务已启动"); return false; } + service.run = true; + + // 控件初始化 + String mode = JProp.getInstance().getString("mode", "loop"); + if ("loop".equals(mode)) { + int intervalValue = JProp.getInstance().getInt("interval", 60); + int timesValue = JProp.getInstance().getInt("times", 1); + scheduler.schedule(service.createTask(), intervalValue, timesValue); + } + if ("timing".equals(mode)) { + String timingValue = JProp.getInstance().getString("timing", ""); + if (timingValue.isEmpty()) { + DialogUtil.alert("请输入定时时间"); + return false; + } + scheduler.schedule(service.createTask(), timingValue); + } - // 4. 指定每天固定时间点执行 - List times = CollUtil.newArrayList("08:00", "12:35", "18:10"); return true; } @@ -104,6 +117,11 @@ public class JMainService { */ public class MacroTask implements Runnable { + /** + * 执行次数 + */ + private int count; + @Override public void run() { if (macro == null) { diff --git a/src/main/java/xyz/wbsite/jmacro/JScheduler.java b/src/main/java/xyz/wbsite/jmacro/JScheduler.java index b21351b..204680d 100644 --- a/src/main/java/xyz/wbsite/jmacro/JScheduler.java +++ b/src/main/java/xyz/wbsite/jmacro/JScheduler.java @@ -1,5 +1,7 @@ package xyz.wbsite.jmacro; +import xyz.wbsite.jmacro.util.Logger; + import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -193,6 +195,9 @@ public class JScheduler { tasksHolder.remove(future); } } + // 自动停止服务 + JMainController.getInstance().onStop(); + Logger.info("任务执行完毕"); } } } @@ -211,9 +216,9 @@ public class JScheduler { // 3, 5); // // // 3. 多时间点任务(每天 08:30:00, 12:00:00, 18:00:00 执行) - scheduler.schedule( - () -> System.out.println("定时任务执行: " + LocalTime.now()), - "12:58:00,18:00:00"); +// scheduler.schedule( +// () -> System.out.println("定时任务执行: " + LocalTime.now()), +// "09:36:00,18:00:00"); // 运行一段时间后关闭 try { diff --git a/src/main/java/xyz/wbsite/jmacro/util/MousePathUtil.java b/src/main/java/xyz/wbsite/jmacro/util/MousePathUtil.java new file mode 100644 index 0000000..b36d8ff --- /dev/null +++ b/src/main/java/xyz/wbsite/jmacro/util/MousePathUtil.java @@ -0,0 +1,124 @@ +package xyz.wbsite.jmacro.util; + +import cn.hutool.core.util.RandomUtil; +import org.sikuli.basics.Settings; +import org.sikuli.script.Location; +import org.sikuli.script.Mouse; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class MousePathUtil { + + private static final Random random = new Random(); + + /** + * 缓动函数(ease-in-out cubic):慢->快->慢 + * f(t) = 3t^2 - 2t^3, t ∈ [0,1] + */ + private static double easeInOut(double t) { + return 3 * t * t - 2 * t * t * t; + } + + /** + * 生成二次贝塞尔曲线点(自动调整步数) + * + * @param x0 起点X + * @param y0 起点Y + * @param cx 控制点X(决定曲线弯曲程度) + * @param cy 控制点Y + * @param x1 终点X + * @param y1 终点Y + * @return 轨迹点集合 + */ + public static List generateBezierPath(int x0, int y0, int cx, int cy, int x1, int y1) { + double distance = Math.hypot(x1 - x0, y1 - y0); + int steps = (int) (distance / 5); // 每5px一个点 + steps = Math.max(20, Math.min(steps, 200)); // 限制范围 + return generateBezierPath(x0, y0, cx, cy, x1, y1, steps); + } + + /** + * 生成二次贝塞尔曲线点(自动调整步数) + * + * @param start 起点 + * @param end 终点 + * @return 轨迹点集合 + */ + public static List generateBezierPath(Location start, Location end) { + int x0 = start.x, y0 = start.y; + int x1 = end.x, y1 = end.y; + // 随机生成控制点,增加不确定性(避免总是直线) + int controlX = (x0 + x1) / 2 + random.nextInt(200) - 100; + int controlY = (y0 + y1) / 2 + random.nextInt(200) - 100; + return generateBezierPath(x0, y0, controlX, controlY, x1, y1); + } + + /** + * 生成二次贝塞尔曲线点(自动调整步数) + * + * @param x0 起点X + * @param y0 起点Y + * @param x1 终点X + * @param y1 终点Y + * @return 轨迹点集合 + */ + public static List generateBezierPath(int x0, int y0, int x1, int y1) { + // 随机生成控制点,增加不确定性(避免总是直线) + int controlX = (x0 + x1) / 2 + random.nextInt(200) - 100; + int controlY = (y0 + y1) / 2 + random.nextInt(200) - 100; + return generateBezierPath(x0, y0, controlX, controlY, x1, y1); + } + + /** + * 生成二次贝塞尔曲线点 + * + * @param x0 起点X + * @param y0 起点Y + * @param cx 控制点X(决定曲线弯曲程度) + * @param cy 控制点Y + * @param x1 终点X + * @param y1 终点Y + * @param steps 基础步数(越大越平滑) + * @return 轨迹点集合 + */ + public static List generateBezierPath(int x0, int y0, int cx, int cy, int x1, int y1, int steps) { + + List path = new ArrayList<>(); + + for (int i = 0; i <= steps; i++) { + // 使用ease函数进行非均匀采样 + double t = (double) i / steps; + double easedT = easeInOut(t); + + // 二次贝塞尔公式 + double xt = Math.pow(1 - easedT, 2) * x0 + 2 * (1 - easedT) * easedT * cx + Math.pow(easedT, 2) * x1; + double yt = Math.pow(1 - easedT, 2) * y0 + 2 * (1 - easedT) * easedT * cy + Math.pow(easedT, 2) * y1; + + // 加入轻微手抖效果 +// xt += random.nextGaussian() * 0.5; +// yt += random.nextGaussian() * 0.5; + + path.add(new int[]{(int) Math.round(xt), (int) Math.round(yt)}); + } + + return path; + } + + public static void main(String[] args) throws InterruptedException { + int startX = 0, startY = 0; + int endX = 500, endY = 500; + + List path = generateBezierPath(startX, startY, endX, endY); + Settings.MoveMouseDelay = 0.0f; + + Mouse.move(new Location(0, 0)); + // 打印路径点 + for (int[] p : path) { + System.out.println(p[0] + "," + p[1]); + Mouse.move(new Location(p[0], p[1])); + Thread.sleep(20); + } + } +} \ No newline at end of file diff --git a/src/main/java/xyz/wbsite/jtask/TaskImpl.java b/src/main/java/xyz/wbsite/jtask/TaskImpl.java index e9e1b7b..50045f3 100644 --- a/src/main/java/xyz/wbsite/jtask/TaskImpl.java +++ b/src/main/java/xyz/wbsite/jtask/TaskImpl.java @@ -38,24 +38,24 @@ public class TaskImpl extends JMacro { Logger.info("启动图标坐标:{}", launch.getRect().toString()); Logger.info("移动鼠标"); mouseMove(launch.getCenter()); - Logger.info("双击我的电脑"); - mouseLeftDoubleClick(launch); - Logger.info("等待程序启动中,请稍等..."); - delay(3 * 1000); - - Region windows = findLegend("我的电脑窗口", 0.9); - if (windows == null) { - Logger.error("我的电脑启动失败"); - return; - } - Logger.info("定位到我的电脑窗口"); - Logger.info("移动鼠标"); - mouseMove(windows.getCenter().offset(100,0)); - - Logger.info("等待1秒后自动关闭"); - delay(1000); - mouseLeftClick(windows.getCenter().offset(100,0)); - - Logger.info("结束任务"); +// Logger.info("双击我的电脑"); +// mouseLeftDoubleClick(launch); +// Logger.info("等待程序启动中,请稍等..."); +// delay(3 * 1000); +// +// Region windows = findLegend("我的电脑窗口", 0.9); +// if (windows == null) { +// Logger.error("我的电脑启动失败"); +// return; +// } +// Logger.info("定位到我的电脑窗口"); +// Logger.info("移动鼠标"); +// mouseMove(windows.getCenter().offset(100,0)); +// +// Logger.info("等待1秒后自动关闭"); +// delay(1000); +// mouseLeftClick(windows.getCenter().offset(100,0)); +// +// Logger.info("结束任务"); } } diff --git a/src/main/resources/main.fxml b/src/main/resources/main.fxml index c58be03..0185669 100644 --- a/src/main/resources/main.fxml +++ b/src/main/resources/main.fxml @@ -4,11 +4,11 @@ + - - + @@ -38,25 +38,25 @@ - - + + + + + +