From ee73106dd154001e83429629d27105ef36e3f1ac Mon Sep 17 00:00:00 2001 From: mengnankkkk Date: Mon, 3 Aug 2026 22:41:09 +0800 Subject: [PATCH 01/19] init --- .../malonetalk/DataAgentApplication.java | 2 + .../ScheduledAgentTaskController.java | 109 ++++++++++ .../dto/ScheduledAgentTaskRequest.java | 30 +++ .../dto/ScheduledAgentTaskResponse.java | 38 ++++ .../dto/ScheduledAgentTaskRunResponse.java | 31 +++ .../malonetalk/entity/ScheduledAgentTask.java | 43 ++++ .../entity/ScheduledAgentTaskRun.java | 35 ++++ .../mapper/ScheduledAgentTaskMapper.java | 59 ++++++ .../mapper/ScheduledAgentTaskRunMapper.java | 41 ++++ .../ScheduledAgentScheduleCalculator.java | 68 ++++++ .../service/ScheduledAgentTaskDispatcher.java | 57 +++++ .../service/ScheduledAgentTaskRunner.java | 190 +++++++++++++++++ .../service/ScheduledAgentTaskService.java | 40 ++++ .../ScheduledAgentTaskServiceImpl.java | 197 ++++++++++++++++++ .../mapper/ScheduledAgentTaskMapper.xml | 108 ++++++++++ .../mapper/ScheduledAgentTaskRunMapper.xml | 42 ++++ sql/scheduled_agent_task.sql | 37 ++++ 17 files changed, 1127 insertions(+) create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskDispatcher.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java create mode 100644 data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml create mode 100644 data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml create mode 100644 sql/scheduled_agent_task.sql diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/DataAgentApplication.java b/data-agent-backend/src/main/java/io/github/malonetalk/DataAgentApplication.java index 4b5d007..6de766c 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/DataAgentApplication.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/DataAgentApplication.java @@ -19,7 +19,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class DataAgentApplication { diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java new file mode 100644 index 0000000..b15a52d --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.controller; + +import io.github.malonetalk.common.Result; +import io.github.malonetalk.dto.ScheduledAgentTaskRequest; +import io.github.malonetalk.dto.ScheduledAgentTaskResponse; +import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; +import io.github.malonetalk.service.ScheduledAgentTaskDispatcher; +import io.github.malonetalk.service.ScheduledAgentTaskService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/scheduled-agent-tasks") +public class ScheduledAgentTaskController { + + private final ScheduledAgentTaskService taskService; + private final ScheduledAgentTaskDispatcher taskDispatcher; + + @PostMapping + public Result create( + @Valid @RequestBody ScheduledAgentTaskRequest request) { + return Result.success(taskService.create(request)); + } + + @PutMapping("/{id}") + public Result update( + @PathVariable @Positive(message = "id must be positive.") Integer id, + @Valid @RequestBody ScheduledAgentTaskRequest request) { + return Result.success(taskService.update(id, request)); + } + + @DeleteMapping("/{id}") + public Result delete( + @PathVariable @Positive(message = "id must be positive.") Integer id) { + taskService.delete(id); + return Result.success(true); + } + + @GetMapping("/{id}") + public Result getById( + @PathVariable @Positive(message = "id must be positive.") Integer id) { + return Result.success(taskService.getById(id)); + } + + @GetMapping + public Result> listAll() { + return Result.success(taskService.listAll()); + } + + @PostMapping("/{id}/enable") + public Result enable( + @PathVariable @Positive(message = "id must be positive.") Integer id) { + taskService.updateEnabled(id, true); + return Result.success(true); + } + + @PostMapping("/{id}/disable") + public Result disable( + @PathVariable @Positive(message = "id must be positive.") Integer id) { + taskService.updateEnabled(id, false); + return Result.success(true); + } + + @PostMapping("/{id}/run") + public Result runNow( + @PathVariable @Positive(message = "id must be positive.") Integer id) { + taskService.getById(id); + taskDispatcher.runNow(id); + return Result.success(true); + } + + @GetMapping("/{id}/runs") + public Result> listRuns( + @PathVariable @Positive(message = "id must be positive.") Integer id, + @RequestParam(defaultValue = "20") int limit) { + return Result.success(taskService.listRuns(id, limit)); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java new file mode 100644 index 0000000..e98a8d5 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ScheduledAgentTaskRequest( + @NotBlank(message = "name cannot be blank.") String name, + @NotBlank(message = "prompt cannot be blank.") String prompt, + @NotBlank(message = "scheduleType cannot be blank.") String scheduleType, + @NotBlank(message = "scheduleExpr cannot be blank.") String scheduleExpr, + String timezone, + Boolean enabled, + String sessionMode, + String sessionId) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java new file mode 100644 index 0000000..458874a --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import java.time.LocalDateTime; + +public record ScheduledAgentTaskResponse( + Integer id, + String name, + String prompt, + String scheduleType, + String scheduleExpr, + String timezone, + Boolean enabled, + Boolean running, + String sessionMode, + String sessionId, + LocalDateTime nextRunAt, + LocalDateTime lastRunAt, + String lastStatus, + String lastError, + LocalDateTime createTime, + LocalDateTime updateTime) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java new file mode 100644 index 0000000..807c58c --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import java.time.LocalDateTime; + +public record ScheduledAgentTaskRunResponse( + Integer id, + Integer taskId, + String sessionId, + String status, + Integer reportId, + String outputSummary, + String errorMessage, + LocalDateTime startedAt, + LocalDateTime finishedAt) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java new file mode 100644 index 0000000..d946cd0 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.entity; + +import java.time.LocalDateTime; +import lombok.Data; + +@Data +public class ScheduledAgentTask { + + private Integer id; + private String name; + private String prompt; + private String scheduleType; + private String scheduleExpr; + private String timezone; + private Boolean enabled; + private Boolean running; + private LocalDateTime lockUntil; + private String sessionMode; + private String sessionId; + private LocalDateTime nextRunAt; + private LocalDateTime lastRunAt; + private String lastStatus; + private String lastError; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java new file mode 100644 index 0000000..79f6466 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.entity; + +import java.time.LocalDateTime; +import lombok.Data; + +@Data +public class ScheduledAgentTaskRun { + + private Integer id; + private Integer taskId; + private String sessionId; + private String status; + private Integer reportId; + private String outputSummary; + private String errorMessage; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java new file mode 100644 index 0000000..d67e246 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.mapper; + +import io.github.malonetalk.entity.ScheduledAgentTask; +import java.time.LocalDateTime; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ScheduledAgentTaskMapper { + + int insert(ScheduledAgentTask task); + + int update(ScheduledAgentTask task); + + int updateEnabled( + @Param("id") Integer id, + @Param("enabled") boolean enabled, + @Param("updateTime") LocalDateTime updateTime); + + int deleteById(@Param("id") Integer id); + + ScheduledAgentTask selectById(@Param("id") Integer id); + + List selectAll(); + + List findDueTasks( + @Param("now") LocalDateTime now, @Param("limit") int limit); + + int lockForRun( + @Param("id") Integer id, + @Param("now") LocalDateTime now, + @Param("lockUntil") LocalDateTime lockUntil, + @Param("force") boolean force); + + int finishRun( + @Param("id") Integer id, + @Param("nextRunAt") LocalDateTime nextRunAt, + @Param("lastRunAt") LocalDateTime lastRunAt, + @Param("lastStatus") String lastStatus, + @Param("lastError") String lastError); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java new file mode 100644 index 0000000..8f9f0d0 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.mapper; + +import io.github.malonetalk.entity.ScheduledAgentTaskRun; +import java.time.LocalDateTime; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ScheduledAgentTaskRunMapper { + + int insert(ScheduledAgentTaskRun run); + + int finish( + @Param("id") Integer id, + @Param("status") String status, + @Param("reportId") Integer reportId, + @Param("outputSummary") String outputSummary, + @Param("errorMessage") String errorMessage, + @Param("finishedAt") LocalDateTime finishedAt); + + List selectByTaskId( + @Param("taskId") Integer taskId, @Param("limit") int limit); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java new file mode 100644 index 0000000..bc2513e --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.exception.BusinessException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.stereotype.Component; + +@Component +public class ScheduledAgentScheduleCalculator { + + public static final String DAILY = "DAILY"; + public static final String INTERVAL = "INTERVAL"; + public static final String CRON = "CRON"; + + public LocalDateTime nextRunAfter(String type, String expr, LocalDateTime after) { + return switch (normalizeType(type)) { + case DAILY -> nextDaily(expr, after); + case INTERVAL -> after.plus(Duration.parse(expr.trim())); + case CRON -> nextCron(expr, after); + default -> throw invalidSchedule("Unsupported schedule type: " + type); + }; + } + + public String normalizeType(String type) { + if (type == null || type.isBlank()) { + throw invalidSchedule("scheduleType cannot be blank."); + } + return type.trim().toUpperCase(); + } + + private LocalDateTime nextDaily(String expr, LocalDateTime after) { + LocalTime time = LocalTime.parse(expr.trim()); + LocalDateTime next = after.toLocalDate().atTime(time); + return next.isAfter(after) ? next : next.plusDays(1); + } + + private LocalDateTime nextCron(String expr, LocalDateTime after) { + LocalDateTime next = CronExpression.parse(expr.trim()).next(after); + if (next == null) { + throw invalidSchedule("Cron expression has no next run time."); + } + return next; + } + + private BusinessException invalidSchedule(String message) { + return BusinessException.of(ErrorCode.BAD_REQUEST, message); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskDispatcher.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskDispatcher.java new file mode 100644 index 0000000..4a06501 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskDispatcher.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.entity.ScheduledAgentTask; +import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; +import jakarta.annotation.PreDestroy; +import java.time.LocalDateTime; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ScheduledAgentTaskDispatcher { + + private static final int BATCH_SIZE = 20; + + private final ScheduledAgentTaskMapper taskMapper; + private final ScheduledAgentTaskRunner taskRunner; + private final ExecutorService executor = Executors.newFixedThreadPool(3); + + @Scheduled(fixedDelayString = "${data-agent.schedule.dispatch-delay-ms:10000}") + public void dispatchDueTasks() { + for (ScheduledAgentTask task : taskMapper.findDueTasks(LocalDateTime.now(), BATCH_SIZE)) { + executor.execute(() -> taskRunner.run(task.getId(), false)); + } + } + + public void runNow(Integer taskId) { + executor.execute(() -> taskRunner.run(taskId, true)); + } + + @PreDestroy + public void shutdown() { + executor.shutdown(); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java new file mode 100644 index 0000000..915e0f4 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.agent.AgentService; +import io.github.malonetalk.agent.tools.ToolCallConstants; +import io.github.malonetalk.dto.ChatStreamEvent; +import io.github.malonetalk.entity.ScheduledAgentTask; +import io.github.malonetalk.entity.ScheduledAgentTaskRun; +import io.github.malonetalk.enums.ChatStreamEventType; +import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; +import io.github.malonetalk.mapper.ScheduledAgentTaskRunMapper; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ScheduledAgentTaskRunner { + + private static final Duration LOCK_DURATION = Duration.ofMinutes(30); + private static final String RUNNING = "RUNNING"; + private static final String SUCCESS = "SUCCESS"; + private static final String FAILED = "FAILED"; + private static final String NEEDS_USER = "NEEDS_USER"; + private static final int SUMMARY_LIMIT = 2000; + + private final AgentService agentService; + private final ScheduledAgentTaskMapper taskMapper; + private final ScheduledAgentTaskRunMapper runMapper; + private final ScheduledAgentScheduleCalculator scheduleCalculator; + + public void run(Integer taskId, boolean force) { + LocalDateTime startedAt = LocalDateTime.now(); + if (taskMapper.lockForRun(taskId, startedAt, startedAt.plus(LOCK_DURATION), force) == 0) { + return; + } + + ScheduledAgentTask task = taskMapper.selectById(taskId); + String sessionId = resolveSessionId(task); + ScheduledAgentTaskRun run = startRun(taskId, sessionId, startedAt); + + String status = SUCCESS; + Integer reportId = null; + String outputSummary = null; + String errorMessage = null; + try { + List events = + agentService + .chatStream(sessionId, buildPrompt(task), null) + .collectList() + .block(); + status = resolveStatus(events); + reportId = extractReportId(events); + outputSummary = limitText(extractOutput(events)); + errorMessage = limitText(extractError(events)); + } catch (Exception e) { + status = FAILED; + errorMessage = limitText(e.getMessage()); + log.error("Scheduled agent task failed: taskId={}", taskId, e); + } finally { + finish(task, run, force, status, reportId, outputSummary, errorMessage); + } + } + + private ScheduledAgentTaskRun startRun( + Integer taskId, String sessionId, LocalDateTime startedAt) { + ScheduledAgentTaskRun run = new ScheduledAgentTaskRun(); + run.setTaskId(taskId); + run.setSessionId(sessionId); + run.setStatus(RUNNING); + run.setStartedAt(startedAt); + runMapper.insert(run); + return run; + } + + private void finish( + ScheduledAgentTask task, + ScheduledAgentTaskRun run, + boolean force, + String status, + Integer reportId, + String outputSummary, + String errorMessage) { + LocalDateTime finishedAt = LocalDateTime.now(); + runMapper.finish(run.getId(), status, reportId, outputSummary, errorMessage, finishedAt); + LocalDateTime nextRunAt = + force + ? task.getNextRunAt() + : scheduleCalculator.nextRunAfter( + task.getScheduleType(), task.getScheduleExpr(), finishedAt); + taskMapper.finishRun(task.getId(), nextRunAt, finishedAt, status, errorMessage); + } + + private String resolveSessionId(ScheduledAgentTask task) { + if (ScheduledAgentTaskServiceImpl.FIXED_SESSION.equals(task.getSessionMode())) { + return task.getSessionId(); + } + return "scheduled-task-" + task.getId() + "-" + UUID.randomUUID(); + } + + private String buildPrompt(ScheduledAgentTask task) { + return task.getPrompt() + "\n\n定时任务执行要求:如果信息不足或需要用户确认,请直接说明无法完成,不要调用 ask_user 反问用户。"; + } + + private String resolveStatus(List events) { + if (events == null) { + return FAILED; + } + if (events.stream().anyMatch(event -> event.type() == ChatStreamEventType.ERROR)) { + return FAILED; + } + if (events.stream().anyMatch(event -> event.type() == ChatStreamEventType.QUESTION)) { + return NEEDS_USER; + } + return SUCCESS; + } + + private Integer extractReportId(List events) { + if (events == null) { + return null; + } + return events.stream() + .filter(event -> event.type() == ChatStreamEventType.REPORT) + .map(ChatStreamEvent::content) + .filter( + content -> + content != null + && content.startsWith(ToolCallConstants.SUCCESS_PREFIX)) + .map(content -> content.substring(ToolCallConstants.SUCCESS_PREFIX.length()).trim()) + .map(Integer::valueOf) + .findFirst() + .orElse(null); + } + + private String extractOutput(List events) { + if (events == null) { + return null; + } + for (int i = events.size() - 1; i >= 0; i--) { + ChatStreamEvent event = events.get(i); + if ((event.type() == ChatStreamEventType.SUMMARY + || event.type() == ChatStreamEventType.TEXT) + && event.content() != null + && !event.content().isBlank()) { + return event.content(); + } + } + return null; + } + + private String extractError(List events) { + if (events == null) { + return "Agent stream returned no events."; + } + return events.stream() + .filter(event -> event.type() == ChatStreamEventType.ERROR) + .map(ChatStreamEvent::content) + .filter(content -> content != null && !content.isBlank()) + .findFirst() + .orElse(null); + } + + private String limitText(String text) { + if (text == null || text.length() <= SUMMARY_LIMIT) { + return text; + } + return text.substring(0, SUMMARY_LIMIT); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java new file mode 100644 index 0000000..eb7cb79 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.dto.ScheduledAgentTaskRequest; +import io.github.malonetalk.dto.ScheduledAgentTaskResponse; +import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; +import java.util.List; + +public interface ScheduledAgentTaskService { + + ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request); + + ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest request); + + void delete(Integer id); + + ScheduledAgentTaskResponse getById(Integer id); + + List listAll(); + + void updateEnabled(Integer id, boolean enabled); + + List listRuns(Integer taskId, int limit); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java new file mode 100644 index 0000000..35fafa4 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.dto.ScheduledAgentTaskRequest; +import io.github.malonetalk.dto.ScheduledAgentTaskResponse; +import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; +import io.github.malonetalk.entity.ScheduledAgentTask; +import io.github.malonetalk.entity.ScheduledAgentTaskRun; +import io.github.malonetalk.exception.BusinessException; +import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; +import io.github.malonetalk.mapper.ScheduledAgentTaskRunMapper; +import io.github.malonetalk.utils.RequestAssert; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ScheduledAgentTaskServiceImpl implements ScheduledAgentTaskService { + + public static final String DEFAULT_TIMEZONE = "Asia/Shanghai"; + public static final String NEW_EACH_RUN = "NEW_EACH_RUN"; + public static final String FIXED_SESSION = "FIXED_SESSION"; + + private final ScheduledAgentTaskMapper taskMapper; + private final ScheduledAgentTaskRunMapper runMapper; + private final ScheduledAgentScheduleCalculator scheduleCalculator; + + @Override + public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { + ScheduledAgentTask task = buildTask(new ScheduledAgentTask(), request); + LocalDateTime now = LocalDateTime.now(); + task.setRunning(false); + task.setNextRunAt( + scheduleCalculator.nextRunAfter( + task.getScheduleType(), task.getScheduleExpr(), now)); + task.setCreateTime(now); + task.setUpdateTime(now); + taskMapper.insert(task); + return toResponse(task); + } + + @Override + public ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest request) { + ScheduledAgentTask existing = getTask(id); + ScheduledAgentTask task = buildTask(existing, request); + task.setNextRunAt( + scheduleCalculator.nextRunAfter( + task.getScheduleType(), task.getScheduleExpr(), LocalDateTime.now())); + task.setUpdateTime(LocalDateTime.now()); + taskMapper.update(task); + return toResponse(taskMapper.selectById(id)); + } + + @Override + public void delete(Integer id) { + RequestAssert.requireNonNegative(id, "id must be non-negative."); + if (taskMapper.deleteById(id) == 0) { + throw notFound(id); + } + } + + @Override + public ScheduledAgentTaskResponse getById(Integer id) { + return toResponse(getTask(id)); + } + + @Override + public List listAll() { + return taskMapper.selectAll().stream().map(this::toResponse).toList(); + } + + @Override + public void updateEnabled(Integer id, boolean enabled) { + getTask(id); + taskMapper.updateEnabled(id, enabled, LocalDateTime.now()); + } + + @Override + public List listRuns(Integer taskId, int limit) { + getTask(taskId); + int resolvedLimit = Math.min(Math.max(limit, 1), 100); + return runMapper.selectByTaskId(taskId, resolvedLimit).stream() + .map(this::toRunResponse) + .toList(); + } + + private ScheduledAgentTask buildTask( + ScheduledAgentTask task, ScheduledAgentTaskRequest request) { + String timezone = + request.timezone() == null || request.timezone().isBlank() + ? DEFAULT_TIMEZONE + : request.timezone().trim(); + ZoneId.of(timezone); + + String sessionMode = normalizeSessionMode(request.sessionMode()); + String sessionId = normalizeSessionId(sessionMode, request.sessionId()); + + task.setName(RequestAssert.requireNotBlank(request.name(), "name cannot be blank.")); + task.setPrompt(RequestAssert.requireNotBlank(request.prompt(), "prompt cannot be blank.")); + task.setScheduleType(scheduleCalculator.normalizeType(request.scheduleType())); + task.setScheduleExpr( + RequestAssert.requireNotBlank( + request.scheduleExpr(), "scheduleExpr cannot be blank.")); + task.setTimezone(timezone); + task.setEnabled(request.enabled() == null || request.enabled()); + task.setSessionMode(sessionMode); + task.setSessionId(sessionId); + return task; + } + + private String normalizeSessionMode(String sessionMode) { + if (sessionMode == null || sessionMode.isBlank()) { + return NEW_EACH_RUN; + } + String normalized = sessionMode.trim().toUpperCase(); + if (!NEW_EACH_RUN.equals(normalized) && !FIXED_SESSION.equals(normalized)) { + throw BusinessException.of( + ErrorCode.BAD_REQUEST, "Unsupported sessionMode: " + sessionMode); + } + return normalized; + } + + private String normalizeSessionId(String sessionMode, String sessionId) { + if (!FIXED_SESSION.equals(sessionMode)) { + return null; + } + return RequestAssert.requireNotBlank( + sessionId, "sessionId is required when sessionMode is FIXED_SESSION."); + } + + private ScheduledAgentTask getTask(Integer id) { + RequestAssert.requireNonNegative(id, "id must be non-negative."); + ScheduledAgentTask task = taskMapper.selectById(id); + if (task == null) { + throw notFound(id); + } + return task; + } + + private BusinessException notFound(Integer id) { + return BusinessException.of( + ErrorCode.RESOURCE_NOT_FOUND, "Scheduled task does not exist: id=" + id); + } + + private ScheduledAgentTaskResponse toResponse(ScheduledAgentTask task) { + return new ScheduledAgentTaskResponse( + task.getId(), + task.getName(), + task.getPrompt(), + task.getScheduleType(), + task.getScheduleExpr(), + task.getTimezone(), + task.getEnabled(), + task.getRunning(), + task.getSessionMode(), + task.getSessionId(), + task.getNextRunAt(), + task.getLastRunAt(), + task.getLastStatus(), + task.getLastError(), + task.getCreateTime(), + task.getUpdateTime()); + } + + private ScheduledAgentTaskRunResponse toRunResponse(ScheduledAgentTaskRun run) { + return new ScheduledAgentTaskRunResponse( + run.getId(), + run.getTaskId(), + run.getSessionId(), + run.getStatus(), + run.getReportId(), + run.getOutputSummary(), + run.getErrorMessage(), + run.getStartedAt(), + run.getFinishedAt()); + } +} diff --git a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml new file mode 100644 index 0000000..6b02dc0 --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO scheduled_agent_task ( + name, prompt, schedule_type, schedule_expr, timezone, + enabled, running, session_mode, session_id, next_run_at, + create_time, update_time + ) VALUES ( + #{name}, #{prompt}, #{scheduleType}, #{scheduleExpr}, #{timezone}, + #{enabled}, #{running}, #{sessionMode}, #{sessionId}, #{nextRunAt}, + #{createTime}, #{updateTime} + ) + + + + UPDATE scheduled_agent_task + SET name = #{name}, + prompt = #{prompt}, + schedule_type = #{scheduleType}, + schedule_expr = #{scheduleExpr}, + timezone = #{timezone}, + enabled = #{enabled}, + session_mode = #{sessionMode}, + session_id = #{sessionId}, + next_run_at = #{nextRunAt}, + update_time = #{updateTime} + WHERE id = #{id} + + + + UPDATE scheduled_agent_task + SET enabled = #{enabled}, update_time = #{updateTime} + WHERE id = #{id} + + + + DELETE FROM scheduled_agent_task + WHERE id = #{id} + + + + + + + + + + UPDATE scheduled_agent_task + SET running = 1, + lock_until = #{lockUntil}, + update_time = #{now} + WHERE id = #{id} + AND (running = 0 OR lock_until < #{now}) + + AND enabled = 1 + AND next_run_at <= #{now} + + + + + UPDATE scheduled_agent_task + SET running = 0, + lock_until = NULL, + next_run_at = #{nextRunAt}, + last_run_at = #{lastRunAt}, + last_status = #{lastStatus}, + last_error = #{lastError}, + update_time = #{lastRunAt} + WHERE id = #{id} + + diff --git a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml new file mode 100644 index 0000000..9254730 --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + INSERT INTO scheduled_agent_task_run ( + task_id, session_id, status, started_at + ) VALUES ( + #{taskId}, #{sessionId}, #{status}, #{startedAt} + ) + + + + UPDATE scheduled_agent_task_run + SET status = #{status}, + report_id = #{reportId}, + output_summary = #{outputSummary}, + error_message = #{errorMessage}, + finished_at = #{finishedAt} + WHERE id = #{id} + + + + diff --git a/sql/scheduled_agent_task.sql b/sql/scheduled_agent_task.sql new file mode 100644 index 0000000..832b454 --- /dev/null +++ b/sql/scheduled_agent_task.sql @@ -0,0 +1,37 @@ +CREATE TABLE IF NOT EXISTS `scheduled_agent_task` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key', + `name` VARCHAR(255) NOT NULL COMMENT 'Task name', + `prompt` TEXT NOT NULL COMMENT 'Prompt sent to the agent on each run', + `schedule_type` VARCHAR(32) NOT NULL COMMENT 'DAILY, INTERVAL, or CRON', + `schedule_expr` VARCHAR(128) NOT NULL COMMENT 'HH:mm[:ss], ISO-8601 duration, or cron', + `timezone` VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT 'Task timezone', + `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether dispatch can pick up the task', + `running` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Whether a run currently owns the task', + `lock_until` DATETIME DEFAULT NULL COMMENT 'Run lock expiration', + `session_mode` VARCHAR(32) NOT NULL DEFAULT 'NEW_EACH_RUN' COMMENT 'NEW_EACH_RUN or FIXED_SESSION', + `session_id` VARCHAR(255) DEFAULT NULL COMMENT 'Fixed session id when session_mode is FIXED_SESSION', + `next_run_at` DATETIME NOT NULL COMMENT 'Next due time', + `last_run_at` DATETIME DEFAULT NULL COMMENT 'Last dispatch time', + `last_status` VARCHAR(32) DEFAULT NULL COMMENT 'Last run status', + `last_error` TEXT DEFAULT NULL COMMENT 'Last run error', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time', + PRIMARY KEY (`id`), + KEY `idx_due_task` (`enabled`, `next_run_at`), + KEY `idx_lock` (`running`, `lock_until`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task'; + +CREATE TABLE IF NOT EXISTS `scheduled_agent_task_run` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key', + `task_id` INT NOT NULL COMMENT 'Task id', + `session_id` VARCHAR(255) NOT NULL COMMENT 'Agent session id', + `status` VARCHAR(32) NOT NULL COMMENT 'RUNNING, SUCCESS, FAILED, or NEEDS_USER', + `report_id` INT DEFAULT NULL COMMENT 'Generated report id', + `output_summary` TEXT DEFAULT NULL COMMENT 'Final agent text or summary', + `error_message` TEXT DEFAULT NULL COMMENT 'Failure reason', + `started_at` DATETIME NOT NULL COMMENT 'Start time', + `finished_at` DATETIME DEFAULT NULL COMMENT 'Finish time', + PRIMARY KEY (`id`), + KEY `idx_task_started_at` (`task_id`, `started_at`), + KEY `idx_session_id` (`session_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task run'; From d7a554f4f8c4d09049f322a17ffbb14fa00ff08e Mon Sep 17 00:00:00 2001 From: mengnankkkk Date: Wed, 5 Aug 2026 20:39:00 +0800 Subject: [PATCH 02/19] feat: add scheduled agent task management - add scheduled task CRUD, dispatch, run history, and owner-based run locking - validate schedule expressions, positive intervals, and timezone-aware next runs - add scheduled task management frontend page - append scheduled task tables to data_source.sql --- .../malonetalk/entity/ScheduledAgentTask.java | 2 + .../mapper/ScheduledAgentTaskMapper.java | 8 + .../ScheduledAgentScheduleCalculator.java | 79 +++- .../service/ScheduledAgentTaskRunner.java | 41 +- .../ScheduledAgentTaskServiceImpl.java | 10 +- .../mapper/ScheduledAgentTaskMapper.xml | 15 + data-agent-frontend/src/api/scheduledTask.ts | 99 ++++ .../src/components/layout/AppSidebar.vue | 1 + data-agent-frontend/src/router/index.ts | 6 + .../scheduled-task/ScheduledTaskManage.vue | 439 ++++++++++++++++++ sql/data_source.sql | 41 ++ sql/scheduled_agent_task.sql | 37 -- 12 files changed, 721 insertions(+), 57 deletions(-) create mode 100644 data-agent-frontend/src/api/scheduledTask.ts create mode 100644 data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue delete mode 100644 sql/scheduled_agent_task.sql diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java index d946cd0..fe58687 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java @@ -32,6 +32,8 @@ public class ScheduledAgentTask { private Boolean enabled; private Boolean running; private LocalDateTime lockUntil; + private String lockOwner; + private Integer currentRunId; private String sessionMode; private String sessionId; private LocalDateTime nextRunAt; diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java index d67e246..c09ac09 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java @@ -48,10 +48,18 @@ int lockForRun( @Param("id") Integer id, @Param("now") LocalDateTime now, @Param("lockUntil") LocalDateTime lockUntil, + @Param("lockOwner") String lockOwner, @Param("force") boolean force); + int markRunStarted( + @Param("id") Integer id, + @Param("lockOwner") String lockOwner, + @Param("currentRunId") Integer currentRunId); + int finishRun( @Param("id") Integer id, + @Param("lockOwner") String lockOwner, + @Param("currentRunId") Integer currentRunId, @Param("nextRunAt") LocalDateTime nextRunAt, @Param("lastRunAt") LocalDateTime lastRunAt, @Param("lastStatus") String lastStatus, diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java index bc2513e..cb9e9f9 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java @@ -19,9 +19,12 @@ import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.exception.BusinessException; +import java.time.DateTimeException; import java.time.Duration; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import org.springframework.scheduling.support.CronExpression; import org.springframework.stereotype.Component; @@ -33,12 +36,33 @@ public class ScheduledAgentScheduleCalculator { public static final String CRON = "CRON"; public LocalDateTime nextRunAfter(String type, String expr, LocalDateTime after) { - return switch (normalizeType(type)) { - case DAILY -> nextDaily(expr, after); - case INTERVAL -> after.plus(Duration.parse(expr.trim())); - case CRON -> nextCron(expr, after); - default -> throw invalidSchedule("Unsupported schedule type: " + type); - }; + return nextRunAfter(type, expr, after, ZoneId.systemDefault().getId()); + } + + public LocalDateTime nextRunAfter( + String type, String expr, LocalDateTime after, String timezone) { + ZoneId taskZone = ZoneId.of(normalizeTimezone(timezone)); + ZoneId storageZone = ZoneId.systemDefault(); + ZonedDateTime afterInTaskZone = after.atZone(storageZone).withZoneSameInstant(taskZone); + ZonedDateTime nextInTaskZone = + switch (normalizeType(type)) { + case DAILY -> nextDaily(expr, afterInTaskZone); + case INTERVAL -> afterInTaskZone.plus(parsePositiveDuration(expr)); + case CRON -> nextCron(expr, afterInTaskZone); + default -> throw invalidSchedule("Unsupported schedule type: " + type); + }; + return nextInTaskZone.withZoneSameInstant(storageZone).toLocalDateTime(); + } + + public String normalizeTimezone(String timezone) { + if (timezone == null || timezone.isBlank()) { + throw invalidSchedule("timezone cannot be blank."); + } + try { + return ZoneId.of(timezone.trim()).getId(); + } catch (DateTimeException e) { + throw invalidSchedule("Unsupported timezone: " + timezone); + } } public String normalizeType(String type) { @@ -48,20 +72,53 @@ public String normalizeType(String type) { return type.trim().toUpperCase(); } - private LocalDateTime nextDaily(String expr, LocalDateTime after) { - LocalTime time = LocalTime.parse(expr.trim()); - LocalDateTime next = after.toLocalDate().atTime(time); + private ZonedDateTime nextDaily(String expr, ZonedDateTime after) { + LocalTime time = parseDailyTime(expr); + ZonedDateTime next = after.toLocalDate().atTime(time).atZone(after.getZone()); return next.isAfter(after) ? next : next.plusDays(1); } - private LocalDateTime nextCron(String expr, LocalDateTime after) { - LocalDateTime next = CronExpression.parse(expr.trim()).next(after); + private ZonedDateTime nextCron(String expr, ZonedDateTime after) { + ZonedDateTime next; + try { + next = CronExpression.parse(requireScheduleExpr(expr)).next(after); + } catch (IllegalArgumentException e) { + throw invalidSchedule("Invalid cron expression: " + expr); + } if (next == null) { throw invalidSchedule("Cron expression has no next run time."); } return next; } + private LocalTime parseDailyTime(String expr) { + try { + return LocalTime.parse(requireScheduleExpr(expr)); + } catch (DateTimeException e) { + throw invalidSchedule("Invalid daily schedule time: " + expr); + } + } + + private Duration parsePositiveDuration(String expr) { + Duration duration; + try { + duration = Duration.parse(requireScheduleExpr(expr)); + } catch (DateTimeException e) { + throw invalidSchedule("Invalid interval duration: " + expr); + } + if (duration.isZero() || duration.isNegative()) { + throw invalidSchedule("Interval duration must be positive."); + } + return duration; + } + + private String requireScheduleExpr(String expr) { + if (expr == null || expr.isBlank()) { + throw invalidSchedule("scheduleExpr cannot be blank."); + } + return expr.trim(); + } + private BusinessException invalidSchedule(String message) { return BusinessException.of(ErrorCode.BAD_REQUEST, message); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java index 915e0f4..c69fb9f 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java @@ -52,13 +52,26 @@ public class ScheduledAgentTaskRunner { public void run(Integer taskId, boolean force) { LocalDateTime startedAt = LocalDateTime.now(); - if (taskMapper.lockForRun(taskId, startedAt, startedAt.plus(LOCK_DURATION), force) == 0) { + String lockOwner = UUID.randomUUID().toString(); + if (taskMapper.lockForRun( + taskId, startedAt, startedAt.plus(LOCK_DURATION), lockOwner, force) + == 0) { return; } ScheduledAgentTask task = taskMapper.selectById(taskId); String sessionId = resolveSessionId(task); ScheduledAgentTaskRun run = startRun(taskId, sessionId, startedAt); + if (taskMapper.markRunStarted(taskId, lockOwner, run.getId()) == 0) { + runMapper.finish( + run.getId(), + FAILED, + null, + null, + "Scheduled task lock was lost before the run started.", + LocalDateTime.now()); + return; + } String status = SUCCESS; Integer reportId = null; @@ -79,7 +92,7 @@ public void run(Integer taskId, boolean force) { errorMessage = limitText(e.getMessage()); log.error("Scheduled agent task failed: taskId={}", taskId, e); } finally { - finish(task, run, force, status, reportId, outputSummary, errorMessage); + finish(task, run, lockOwner, force, status, reportId, outputSummary, errorMessage); } } @@ -97,6 +110,7 @@ private ScheduledAgentTaskRun startRun( private void finish( ScheduledAgentTask task, ScheduledAgentTaskRun run, + String lockOwner, boolean force, String status, Integer reportId, @@ -105,11 +119,28 @@ private void finish( LocalDateTime finishedAt = LocalDateTime.now(); runMapper.finish(run.getId(), status, reportId, outputSummary, errorMessage, finishedAt); LocalDateTime nextRunAt = - force + force && task.getNextRunAt().isAfter(finishedAt) ? task.getNextRunAt() : scheduleCalculator.nextRunAfter( - task.getScheduleType(), task.getScheduleExpr(), finishedAt); - taskMapper.finishRun(task.getId(), nextRunAt, finishedAt, status, errorMessage); + task.getScheduleType(), + task.getScheduleExpr(), + finishedAt, + task.getTimezone()); + int updated = + taskMapper.finishRun( + task.getId(), + lockOwner, + run.getId(), + nextRunAt, + finishedAt, + status, + errorMessage); + if (updated == 0) { + log.warn( + "Scheduled agent task lock changed before finish: taskId={}, runId={}", + task.getId(), + run.getId()); + } } private String resolveSessionId(ScheduledAgentTask task) { diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java index 35fafa4..b68b0dc 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java @@ -28,7 +28,6 @@ import io.github.malonetalk.mapper.ScheduledAgentTaskRunMapper; import io.github.malonetalk.utils.RequestAssert; import java.time.LocalDateTime; -import java.time.ZoneId; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -52,7 +51,7 @@ public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { task.setRunning(false); task.setNextRunAt( scheduleCalculator.nextRunAfter( - task.getScheduleType(), task.getScheduleExpr(), now)); + task.getScheduleType(), task.getScheduleExpr(), now, task.getTimezone())); task.setCreateTime(now); task.setUpdateTime(now); taskMapper.insert(task); @@ -65,7 +64,10 @@ public ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest r ScheduledAgentTask task = buildTask(existing, request); task.setNextRunAt( scheduleCalculator.nextRunAfter( - task.getScheduleType(), task.getScheduleExpr(), LocalDateTime.now())); + task.getScheduleType(), + task.getScheduleExpr(), + LocalDateTime.now(), + task.getTimezone())); task.setUpdateTime(LocalDateTime.now()); taskMapper.update(task); return toResponse(taskMapper.selectById(id)); @@ -110,7 +112,7 @@ private ScheduledAgentTask buildTask( request.timezone() == null || request.timezone().isBlank() ? DEFAULT_TIMEZONE : request.timezone().trim(); - ZoneId.of(timezone); + timezone = scheduleCalculator.normalizeTimezone(timezone); String sessionMode = normalizeSessionMode(request.sessionMode()); String sessionId = normalizeSessionId(sessionMode, request.sessionId()); diff --git a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml index 6b02dc0..e53f1a8 100644 --- a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml @@ -12,6 +12,8 @@ + + @@ -85,6 +87,8 @@ UPDATE scheduled_agent_task SET running = 1, lock_until = #{lockUntil}, + lock_owner = #{lockOwner}, + current_run_id = NULL, update_time = #{now} WHERE id = #{id} AND (running = 0 OR lock_until < #{now}) @@ -94,15 +98,26 @@ + + UPDATE scheduled_agent_task + SET current_run_id = #{currentRunId} + WHERE id = #{id} + AND lock_owner = #{lockOwner} + + UPDATE scheduled_agent_task SET running = 0, lock_until = NULL, + lock_owner = NULL, + current_run_id = NULL, next_run_at = #{nextRunAt}, last_run_at = #{lastRunAt}, last_status = #{lastStatus}, last_error = #{lastError}, update_time = #{lastRunAt} WHERE id = #{id} + AND lock_owner = #{lockOwner} + AND current_run_id = #{currentRunId} diff --git a/data-agent-frontend/src/api/scheduledTask.ts b/data-agent-frontend/src/api/scheduledTask.ts new file mode 100644 index 0000000..87a85a7 --- /dev/null +++ b/data-agent-frontend/src/api/scheduledTask.ts @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import request from './request'; +import type { ApiResponse } from './request'; + +export type ScheduleType = 'DAILY' | 'INTERVAL' | 'CRON'; +export type SessionMode = 'NEW_EACH_RUN' | 'FIXED_SESSION'; + +export interface ScheduledTaskRequest { + name: string; + prompt: string; + scheduleType: ScheduleType; + scheduleExpr: string; + timezone?: string; + enabled?: boolean; + sessionMode?: SessionMode; + sessionId?: string; +} + +export interface ScheduledTaskResponse { + id: number; + name: string; + prompt: string; + scheduleType: ScheduleType; + scheduleExpr: string; + timezone: string; + enabled: boolean; + running: boolean; + sessionMode: SessionMode; + sessionId: string | null; + nextRunAt: string; + lastRunAt: string | null; + lastStatus: string | null; + lastError: string | null; + createTime: string; + updateTime: string; +} + +export interface ScheduledTaskRunResponse { + id: number; + taskId: number; + sessionId: string; + status: string; + reportId: number | null; + outputSummary: string | null; + errorMessage: string | null; + startedAt: string; + finishedAt: string | null; +} + +export function listScheduledTasks() { + return request.get>('/scheduled-agent-tasks'); +} + +export function createScheduledTask(data: ScheduledTaskRequest) { + return request.post>('/scheduled-agent-tasks', data); +} + +export function updateScheduledTask(id: number, data: ScheduledTaskRequest) { + return request.put>(`/scheduled-agent-tasks/${id}`, data); +} + +export function deleteScheduledTask(id: number) { + return request.delete>(`/scheduled-agent-tasks/${id}`); +} + +export function enableScheduledTask(id: number) { + return request.post>(`/scheduled-agent-tasks/${id}/enable`); +} + +export function disableScheduledTask(id: number) { + return request.post>(`/scheduled-agent-tasks/${id}/disable`); +} + +export function runScheduledTask(id: number) { + return request.post>(`/scheduled-agent-tasks/${id}/run`); +} + +export function listScheduledTaskRuns(id: number, limit = 20) { + return request.get>( + `/scheduled-agent-tasks/${id}/runs`, + { params: { limit } }, + ); +} diff --git a/data-agent-frontend/src/components/layout/AppSidebar.vue b/data-agent-frontend/src/components/layout/AppSidebar.vue index 5bf2dec..9b3b748 100644 --- a/data-agent-frontend/src/components/layout/AppSidebar.vue +++ b/data-agent-frontend/src/components/layout/AppSidebar.vue @@ -30,6 +30,7 @@ { path: '/semantic', title: '语义管理', icon: 'Collection' }, { path: '/report', title: '报告管理', icon: 'Document' }, { path: '/metric', title: '指标口径管理', icon: 'DataLine' }, + { path: '/scheduled-task', title: '定时任务', icon: 'Timer' }, ]; const activeMenu = computed(() => route.path); diff --git a/data-agent-frontend/src/router/index.ts b/data-agent-frontend/src/router/index.ts index f37dd38..9deb185 100644 --- a/data-agent-frontend/src/router/index.ts +++ b/data-agent-frontend/src/router/index.ts @@ -59,6 +59,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/metric/MetricManage.vue'), meta: { title: '指标口径管理' }, }, + { + path: '/scheduled-task', + name: 'ScheduledTaskManage', + component: () => import('@/views/scheduled-task/ScheduledTaskManage.vue'), + meta: { title: '定时任务' }, + }, ]; const router = createRouter({ diff --git a/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue new file mode 100644 index 0000000..9223a21 --- /dev/null +++ b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue @@ -0,0 +1,439 @@ + + + + + + + diff --git a/sql/data_source.sql b/sql/data_source.sql index 5b93723..160ac0f 100644 --- a/sql/data_source.sql +++ b/sql/data_source.sql @@ -105,3 +105,44 @@ CREATE TABLE IF NOT EXISTS `report` ( PRIMARY KEY (`id`), KEY `idx_session_id` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报告表'; + +CREATE TABLE IF NOT EXISTS `scheduled_agent_task` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key', + `name` VARCHAR(255) NOT NULL COMMENT 'Task name', + `prompt` TEXT NOT NULL COMMENT 'Prompt sent to the agent on each run', + `schedule_type` VARCHAR(32) NOT NULL COMMENT 'DAILY, INTERVAL, or CRON', + `schedule_expr` VARCHAR(128) NOT NULL COMMENT 'HH:mm[:ss], ISO-8601 duration, or cron', + `timezone` VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT 'Task timezone', + `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether dispatch can pick up the task', + `running` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Whether a run currently owns the task', + `lock_until` DATETIME DEFAULT NULL COMMENT 'Run lock expiration', + `lock_owner` VARCHAR(64) DEFAULT NULL COMMENT 'Run lock owner token', + `current_run_id` INT DEFAULT NULL COMMENT 'Current run id while running', + `session_mode` VARCHAR(32) NOT NULL DEFAULT 'NEW_EACH_RUN' COMMENT 'NEW_EACH_RUN or FIXED_SESSION', + `session_id` VARCHAR(255) DEFAULT NULL COMMENT 'Fixed session id when session_mode is FIXED_SESSION', + `next_run_at` DATETIME NOT NULL COMMENT 'Next due time', + `last_run_at` DATETIME DEFAULT NULL COMMENT 'Last dispatch time', + `last_status` VARCHAR(32) DEFAULT NULL COMMENT 'Last run status', + `last_error` TEXT DEFAULT NULL COMMENT 'Last run error', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time', + PRIMARY KEY (`id`), + KEY `idx_due_task` (`enabled`, `next_run_at`), + KEY `idx_lock` (`running`, `lock_until`), + KEY `idx_lock_owner` (`lock_owner`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task'; + +CREATE TABLE IF NOT EXISTS `scheduled_agent_task_run` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key', + `task_id` INT NOT NULL COMMENT 'Task id', + `session_id` VARCHAR(255) NOT NULL COMMENT 'Agent session id', + `status` VARCHAR(32) NOT NULL COMMENT 'RUNNING, SUCCESS, FAILED, or NEEDS_USER', + `report_id` INT DEFAULT NULL COMMENT 'Generated report id', + `output_summary` TEXT DEFAULT NULL COMMENT 'Final agent text or summary', + `error_message` TEXT DEFAULT NULL COMMENT 'Failure reason', + `started_at` DATETIME NOT NULL COMMENT 'Start time', + `finished_at` DATETIME DEFAULT NULL COMMENT 'Finish time', + PRIMARY KEY (`id`), + KEY `idx_task_started_at` (`task_id`, `started_at`), + KEY `idx_session_id` (`session_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task run'; diff --git a/sql/scheduled_agent_task.sql b/sql/scheduled_agent_task.sql deleted file mode 100644 index 832b454..0000000 --- a/sql/scheduled_agent_task.sql +++ /dev/null @@ -1,37 +0,0 @@ -CREATE TABLE IF NOT EXISTS `scheduled_agent_task` ( - `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key', - `name` VARCHAR(255) NOT NULL COMMENT 'Task name', - `prompt` TEXT NOT NULL COMMENT 'Prompt sent to the agent on each run', - `schedule_type` VARCHAR(32) NOT NULL COMMENT 'DAILY, INTERVAL, or CRON', - `schedule_expr` VARCHAR(128) NOT NULL COMMENT 'HH:mm[:ss], ISO-8601 duration, or cron', - `timezone` VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT 'Task timezone', - `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether dispatch can pick up the task', - `running` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Whether a run currently owns the task', - `lock_until` DATETIME DEFAULT NULL COMMENT 'Run lock expiration', - `session_mode` VARCHAR(32) NOT NULL DEFAULT 'NEW_EACH_RUN' COMMENT 'NEW_EACH_RUN or FIXED_SESSION', - `session_id` VARCHAR(255) DEFAULT NULL COMMENT 'Fixed session id when session_mode is FIXED_SESSION', - `next_run_at` DATETIME NOT NULL COMMENT 'Next due time', - `last_run_at` DATETIME DEFAULT NULL COMMENT 'Last dispatch time', - `last_status` VARCHAR(32) DEFAULT NULL COMMENT 'Last run status', - `last_error` TEXT DEFAULT NULL COMMENT 'Last run error', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time', - `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time', - PRIMARY KEY (`id`), - KEY `idx_due_task` (`enabled`, `next_run_at`), - KEY `idx_lock` (`running`, `lock_until`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task'; - -CREATE TABLE IF NOT EXISTS `scheduled_agent_task_run` ( - `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key', - `task_id` INT NOT NULL COMMENT 'Task id', - `session_id` VARCHAR(255) NOT NULL COMMENT 'Agent session id', - `status` VARCHAR(32) NOT NULL COMMENT 'RUNNING, SUCCESS, FAILED, or NEEDS_USER', - `report_id` INT DEFAULT NULL COMMENT 'Generated report id', - `output_summary` TEXT DEFAULT NULL COMMENT 'Final agent text or summary', - `error_message` TEXT DEFAULT NULL COMMENT 'Failure reason', - `started_at` DATETIME NOT NULL COMMENT 'Start time', - `finished_at` DATETIME DEFAULT NULL COMMENT 'Finish time', - PRIMARY KEY (`id`), - KEY `idx_task_started_at` (`task_id`, `started_at`), - KEY `idx_session_id` (`session_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task run'; From ded215996a6c7ffb6ac1c11cce2dd48edd051208 Mon Sep 17 00:00:00 2001 From: mengnankkkk Date: Fri, 7 Aug 2026 20:20:08 +0800 Subject: [PATCH 03/19] fix --- .../ScheduledAgentTaskController.java | 6 +- ...sePollingScheduledAgentTaskScheduler.java} | 27 +++++++- .../service/ScheduledAgentTaskScheduler.java | 65 +++++++++++++++++++ .../ScheduledAgentTaskSchedulerStrategy.java | 31 +++++++++ .../ScheduledAgentTaskServiceImpl.java | 11 +++- 5 files changed, 134 insertions(+), 6 deletions(-) rename data-agent-backend/src/main/java/io/github/malonetalk/service/{ScheduledAgentTaskDispatcher.java => DatabasePollingScheduledAgentTaskScheduler.java} (72%) create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskSchedulerStrategy.java diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java index b15a52d..b52de84 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java @@ -21,7 +21,7 @@ import io.github.malonetalk.dto.ScheduledAgentTaskRequest; import io.github.malonetalk.dto.ScheduledAgentTaskResponse; import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; -import io.github.malonetalk.service.ScheduledAgentTaskDispatcher; +import io.github.malonetalk.service.ScheduledAgentTaskScheduler; import io.github.malonetalk.service.ScheduledAgentTaskService; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; @@ -45,7 +45,7 @@ public class ScheduledAgentTaskController { private final ScheduledAgentTaskService taskService; - private final ScheduledAgentTaskDispatcher taskDispatcher; + private final ScheduledAgentTaskScheduler taskScheduler; @PostMapping public Result create( @@ -96,7 +96,7 @@ public Result disable( public Result runNow( @PathVariable @Positive(message = "id must be positive.") Integer id) { taskService.getById(id); - taskDispatcher.runNow(id); + taskScheduler.runNow(id); return Result.success(true); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskDispatcher.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java similarity index 72% rename from data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskDispatcher.java rename to data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java index 4a06501..b4eca30 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskDispatcher.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java @@ -25,27 +25,52 @@ import java.util.concurrent.Executors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Slf4j @Component @RequiredArgsConstructor -public class ScheduledAgentTaskDispatcher { +public class DatabasePollingScheduledAgentTaskScheduler + implements ScheduledAgentTaskSchedulerStrategy { + private static final String TYPE = "db-polling"; private static final int BATCH_SIZE = 20; private final ScheduledAgentTaskMapper taskMapper; private final ScheduledAgentTaskRunner taskRunner; private final ExecutorService executor = Executors.newFixedThreadPool(3); + @Value("${data-agent.schedule.scheduler-type:" + TYPE + "}") + private String schedulerType; + + @Override + public String type() { + return TYPE; + } + + @Override + public void sync(ScheduledAgentTask task) { + // DB polling reads the task table on each tick, so persistence is the schedule. + } + + @Override + public void unschedule(Integer taskId) { + // DB polling has no external job to remove. + } + @Scheduled(fixedDelayString = "${data-agent.schedule.dispatch-delay-ms:10000}") public void dispatchDueTasks() { + if (!TYPE.equalsIgnoreCase(schedulerType.trim())) { + return; + } for (ScheduledAgentTask task : taskMapper.findDueTasks(LocalDateTime.now(), BATCH_SIZE)) { executor.execute(() -> taskRunner.run(task.getId(), false)); } } + @Override public void runNow(Integer taskId) { executor.execute(() -> taskRunner.run(taskId, true)); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java new file mode 100644 index 0000000..e488d20 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.entity.ScheduledAgentTask; +import java.util.List; +import java.util.Locale; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class ScheduledAgentTaskScheduler { + + private final ScheduledAgentTaskSchedulerStrategy selectedStrategy; + + public ScheduledAgentTaskScheduler( + List strategies, + @Value("${data-agent.schedule.scheduler-type:db-polling}") String schedulerType) { + this.selectedStrategy = select(strategies, schedulerType); + } + + public void sync(ScheduledAgentTask task) { + selectedStrategy.sync(task); + } + + public void unschedule(Integer taskId) { + selectedStrategy.unschedule(taskId); + } + + public void runNow(Integer taskId) { + selectedStrategy.runNow(taskId); + } + + private ScheduledAgentTaskSchedulerStrategy select( + List strategies, String schedulerType) { + String selectedType = normalize(schedulerType); + return strategies.stream() + .filter(strategy -> selectedType.equals(normalize(strategy.type()))) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + "Unsupported scheduled task scheduler type: " + + schedulerType)); + } + + private String normalize(String value) { + return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskSchedulerStrategy.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskSchedulerStrategy.java new file mode 100644 index 0000000..b31946d --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskSchedulerStrategy.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.entity.ScheduledAgentTask; + +public interface ScheduledAgentTaskSchedulerStrategy { + + String type(); + + void sync(ScheduledAgentTask task); + + void unschedule(Integer taskId); + + void runNow(Integer taskId); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java index b68b0dc..2a21650 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java @@ -43,6 +43,7 @@ public class ScheduledAgentTaskServiceImpl implements ScheduledAgentTaskService private final ScheduledAgentTaskMapper taskMapper; private final ScheduledAgentTaskRunMapper runMapper; private final ScheduledAgentScheduleCalculator scheduleCalculator; + private final ScheduledAgentTaskScheduler taskScheduler; @Override public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { @@ -55,6 +56,7 @@ public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { task.setCreateTime(now); task.setUpdateTime(now); taskMapper.insert(task); + taskScheduler.sync(task); return toResponse(task); } @@ -70,7 +72,9 @@ public ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest r task.getTimezone())); task.setUpdateTime(LocalDateTime.now()); taskMapper.update(task); - return toResponse(taskMapper.selectById(id)); + ScheduledAgentTask saved = taskMapper.selectById(id); + taskScheduler.sync(saved); + return toResponse(saved); } @Override @@ -79,6 +83,7 @@ public void delete(Integer id) { if (taskMapper.deleteById(id) == 0) { throw notFound(id); } + taskScheduler.unschedule(id); } @Override @@ -93,8 +98,10 @@ public List listAll() { @Override public void updateEnabled(Integer id, boolean enabled) { - getTask(id); + ScheduledAgentTask task = getTask(id); taskMapper.updateEnabled(id, enabled, LocalDateTime.now()); + task.setEnabled(enabled); + taskScheduler.sync(task); } @Override From 45a9664b024fd86e9a4354144faa68ce93958db8 Mon Sep 17 00:00:00 2001 From: mengnankkkk Date: Fri, 7 Aug 2026 21:09:02 +0800 Subject: [PATCH 04/19] fix --- .../github/malonetalk/agent/AgentService.java | 17 +++- .../malonetalk/agent/ToolCallContext.java | 5 +- .../malonetalk/agent/tools/AskUserTool.java | 8 +- .../ScheduledAgentTaskController.java | 7 +- .../dto/ScheduledAgentTaskRequest.java | 7 +- .../dto/ScheduledAgentTaskResponse.java | 9 +- .../dto/ScheduledAgentTaskRunResponse.java | 3 +- .../enums/ScheduledAgentScheduleType.java | 37 +++++++ .../enums/ScheduledAgentSessionMode.java | 36 +++++++ .../ScheduledAgentTaskStatus.java} | 18 ++-- .../mapper/ScheduledAgentTaskMapper.java | 6 ++ ...asePollingScheduledAgentTaskScheduler.java | 80 +++++++++------ .../ScheduledAgentScheduleCalculator.java | 19 ++-- .../service/ScheduledAgentTaskRunner.java | 97 ++++++++++++++----- .../service/ScheduledAgentTaskScheduler.java | 65 ------------- .../ScheduledAgentTaskServiceImpl.java | 48 ++++----- .../src/main/resources/application.properties | 6 ++ .../mapper/ScheduledAgentTaskMapper.xml | 12 +++ .../scheduled-task/ScheduledTaskManage.vue | 8 +- 19 files changed, 298 insertions(+), 190 deletions(-) create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java rename data-agent-backend/src/main/java/io/github/malonetalk/{service/ScheduledAgentTaskSchedulerStrategy.java => enums/ScheduledAgentTaskStatus.java} (72%) delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java index 8038dbc..6b0cb7f 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java @@ -74,13 +74,24 @@ public void init() { public Flux chatStream( String sessionId, String userInput, List toolResults) { - return Flux.defer(() -> streamAgent(sessionId, userInput, toolResults)) + return chatStream(sessionId, userInput, toolResults, true); + } + + public Flux chatStream( + String sessionId, + String userInput, + List toolResults, + boolean allowUserPrompt) { + return Flux.defer(() -> streamAgent(sessionId, userInput, toolResults, allowUserPrompt)) .onErrorResume(this::toErrorEvent); } private Flux streamAgent( - String sessionId, String userInput, List toolResults) { - ReActAgent agent = createAgent(ToolCallContext.builder().sessionId(sessionId).build()); + String sessionId, + String userInput, + List toolResults, + boolean allowUserPrompt) { + ReActAgent agent = createAgent(new ToolCallContext(sessionId, allowUserPrompt)); Session session = sessionService.getOrCreateSession(sessionId); agent.loadIfExists(session, sessionId); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/ToolCallContext.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/ToolCallContext.java index 58d7a32..4669f3f 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/ToolCallContext.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/ToolCallContext.java @@ -17,7 +17,4 @@ */ package io.github.malonetalk.agent; -import lombok.Builder; - -@Builder -public record ToolCallContext(String sessionId) {} +public record ToolCallContext(String sessionId, boolean allowUserPrompt) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/AskUserTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/AskUserTool.java index faf47d8..38274ca 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/AskUserTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/AskUserTool.java @@ -20,6 +20,7 @@ import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; import io.agentscope.core.tool.ToolSuspendException; +import io.github.malonetalk.agent.ToolCallContext; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -34,8 +35,13 @@ public class AskUserTool implements MarkAgentTool { + " confirmation. Execution resumes after the user responds.") public String askUser( @ToolParam(name = "question", description = "The question to ask the user.") - String question) { + String question, + ToolCallContext ctx) { log.info("Agent asks user: {}", question); + if (!ctx.allowUserPrompt()) { + return "Cannot ask the user during this run. Explain what information is missing and" + + " stop."; + } throw new ToolSuspendException(question); } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java index b52de84..42b8e8f 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java @@ -21,7 +21,7 @@ import io.github.malonetalk.dto.ScheduledAgentTaskRequest; import io.github.malonetalk.dto.ScheduledAgentTaskResponse; import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; -import io.github.malonetalk.service.ScheduledAgentTaskScheduler; +import io.github.malonetalk.service.DatabasePollingScheduledAgentTaskScheduler; import io.github.malonetalk.service.ScheduledAgentTaskService; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; @@ -45,7 +45,7 @@ public class ScheduledAgentTaskController { private final ScheduledAgentTaskService taskService; - private final ScheduledAgentTaskScheduler taskScheduler; + private final DatabasePollingScheduledAgentTaskScheduler taskScheduler; @PostMapping public Result create( @@ -96,8 +96,7 @@ public Result disable( public Result runNow( @PathVariable @Positive(message = "id must be positive.") Integer id) { taskService.getById(id); - taskScheduler.runNow(id); - return Result.success(true); + return Result.success(taskScheduler.runNow(id)); } @GetMapping("/{id}/runs") diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java index e98a8d5..31f9753 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java @@ -17,14 +17,17 @@ */ package io.github.malonetalk.dto; +import io.github.malonetalk.enums.ScheduledAgentScheduleType; +import io.github.malonetalk.enums.ScheduledAgentSessionMode; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record ScheduledAgentTaskRequest( @NotBlank(message = "name cannot be blank.") String name, @NotBlank(message = "prompt cannot be blank.") String prompt, - @NotBlank(message = "scheduleType cannot be blank.") String scheduleType, + @NotNull(message = "scheduleType cannot be null.") ScheduledAgentScheduleType scheduleType, @NotBlank(message = "scheduleExpr cannot be blank.") String scheduleExpr, String timezone, Boolean enabled, - String sessionMode, + ScheduledAgentSessionMode sessionMode, String sessionId) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java index 458874a..0202bbf 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java @@ -17,22 +17,25 @@ */ package io.github.malonetalk.dto; +import io.github.malonetalk.enums.ScheduledAgentScheduleType; +import io.github.malonetalk.enums.ScheduledAgentSessionMode; +import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import java.time.LocalDateTime; public record ScheduledAgentTaskResponse( Integer id, String name, String prompt, - String scheduleType, + ScheduledAgentScheduleType scheduleType, String scheduleExpr, String timezone, Boolean enabled, Boolean running, - String sessionMode, + ScheduledAgentSessionMode sessionMode, String sessionId, LocalDateTime nextRunAt, LocalDateTime lastRunAt, - String lastStatus, + ScheduledAgentTaskStatus lastStatus, String lastError, LocalDateTime createTime, LocalDateTime updateTime) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java index 807c58c..bde47d6 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java @@ -17,13 +17,14 @@ */ package io.github.malonetalk.dto; +import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import java.time.LocalDateTime; public record ScheduledAgentTaskRunResponse( Integer id, Integer taskId, String sessionId, - String status, + ScheduledAgentTaskStatus status, Integer reportId, String outputSummary, String errorMessage, diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java new file mode 100644 index 0000000..3d2a60f --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.enums; + +import java.util.Locale; + +public enum ScheduledAgentScheduleType { + DAILY, + INTERVAL, + CRON; + + public static ScheduledAgentScheduleType from(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("scheduleType cannot be blank."); + } + try { + return valueOf(value.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Unsupported schedule type: " + value, e); + } + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java new file mode 100644 index 0000000..e15266c --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.enums; + +import java.util.Locale; + +public enum ScheduledAgentSessionMode { + NEW_EACH_RUN, + FIXED_SESSION; + + public static ScheduledAgentSessionMode fromOrDefault(String value) { + if (value == null || value.isBlank()) { + return NEW_EACH_RUN; + } + try { + return valueOf(value.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Unsupported sessionMode: " + value, e); + } + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskSchedulerStrategy.java b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java similarity index 72% rename from data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskSchedulerStrategy.java rename to data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java index b31946d..cc81fbd 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskSchedulerStrategy.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java @@ -15,17 +15,11 @@ * along with this program. If not, see . * limitations under the License. */ -package io.github.malonetalk.service; +package io.github.malonetalk.enums; -import io.github.malonetalk.entity.ScheduledAgentTask; - -public interface ScheduledAgentTaskSchedulerStrategy { - - String type(); - - void sync(ScheduledAgentTask task); - - void unschedule(Integer taskId); - - void runNow(Integer taskId); +public enum ScheduledAgentTaskStatus { + RUNNING, + SUCCESS, + FAILED, + NEEDS_USER } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java index c09ac09..248836e 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java @@ -64,4 +64,10 @@ int finishRun( @Param("lastRunAt") LocalDateTime lastRunAt, @Param("lastStatus") String lastStatus, @Param("lastError") String lastError); + + int releaseClaim( + @Param("id") Integer id, + @Param("lockOwner") String lockOwner, + @Param("currentRunId") Integer currentRunId, + @Param("updateTime") LocalDateTime updateTime); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java index b4eca30..bfc53c5 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java @@ -17,62 +17,88 @@ */ package io.github.malonetalk.service; -import io.github.malonetalk.entity.ScheduledAgentTask; import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; +import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import java.time.LocalDateTime; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.task.TaskRejectedException; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; @Slf4j @Component @RequiredArgsConstructor -public class DatabasePollingScheduledAgentTaskScheduler - implements ScheduledAgentTaskSchedulerStrategy { +public class DatabasePollingScheduledAgentTaskScheduler { - private static final String TYPE = "db-polling"; private static final int BATCH_SIZE = 20; private final ScheduledAgentTaskMapper taskMapper; private final ScheduledAgentTaskRunner taskRunner; - private final ExecutorService executor = Executors.newFixedThreadPool(3); + private final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - @Value("${data-agent.schedule.scheduler-type:" + TYPE + "}") - private String schedulerType; + @Value("${data-agent.schedule.executor.core-size:3}") + private int corePoolSize; - @Override - public String type() { - return TYPE; - } + @Value("${data-agent.schedule.executor.max-size:3}") + private int maxPoolSize; - @Override - public void sync(ScheduledAgentTask task) { - // DB polling reads the task table on each tick, so persistence is the schedule. - } + @Value("${data-agent.schedule.executor.queue-capacity:20}") + private int queueCapacity; - @Override - public void unschedule(Integer taskId) { - // DB polling has no external job to remove. + @PostConstruct + public void initExecutor() { + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setThreadNamePrefix("scheduled-agent-task-"); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); + executor.initialize(); } @Scheduled(fixedDelayString = "${data-agent.schedule.dispatch-delay-ms:10000}") public void dispatchDueTasks() { - if (!TYPE.equalsIgnoreCase(schedulerType.trim())) { - return; + for (var task : taskMapper.findDueTasks(LocalDateTime.now(), BATCH_SIZE)) { + if (!hasCapacity()) { + return; + } + ScheduledAgentTaskRunner.ClaimedRun claimedRun = taskRunner.claim(task.getId(), false); + if (claimedRun != null && !execute(claimedRun)) { + return; + } + } + } + + public boolean runNow(Integer taskId) { + if (!hasCapacity()) { + return false; + } + ScheduledAgentTaskRunner.ClaimedRun claimedRun = taskRunner.claim(taskId, true); + if (claimedRun == null) { + return false; } - for (ScheduledAgentTask task : taskMapper.findDueTasks(LocalDateTime.now(), BATCH_SIZE)) { - executor.execute(() -> taskRunner.run(task.getId(), false)); + return execute(claimedRun); + } + + private boolean execute(ScheduledAgentTaskRunner.ClaimedRun claimedRun) { + try { + executor.execute(() -> taskRunner.run(claimedRun)); + return true; + } catch (TaskRejectedException e) { + taskRunner.reject(claimedRun, "Scheduled task executor rejected the run."); + log.warn("Scheduled agent task executor rejected taskId={}", claimedRun.task().getId()); + return false; } } - @Override - public void runNow(Integer taskId) { - executor.execute(() -> taskRunner.run(taskId, true)); + private boolean hasCapacity() { + ThreadPoolExecutor pool = executor.getThreadPoolExecutor(); + return pool.getActiveCount() < pool.getMaximumPoolSize() + || pool.getQueue().remainingCapacity() > 0; } @PreDestroy diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java index cb9e9f9..6dfe4dd 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java @@ -18,6 +18,7 @@ package io.github.malonetalk.service; import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.enums.ScheduledAgentScheduleType; import io.github.malonetalk.exception.BusinessException; import java.time.DateTimeException; import java.time.Duration; @@ -31,14 +32,6 @@ @Component public class ScheduledAgentScheduleCalculator { - public static final String DAILY = "DAILY"; - public static final String INTERVAL = "INTERVAL"; - public static final String CRON = "CRON"; - - public LocalDateTime nextRunAfter(String type, String expr, LocalDateTime after) { - return nextRunAfter(type, expr, after, ZoneId.systemDefault().getId()); - } - public LocalDateTime nextRunAfter( String type, String expr, LocalDateTime after, String timezone) { ZoneId taskZone = ZoneId.of(normalizeTimezone(timezone)); @@ -49,7 +42,6 @@ public LocalDateTime nextRunAfter( case DAILY -> nextDaily(expr, afterInTaskZone); case INTERVAL -> afterInTaskZone.plus(parsePositiveDuration(expr)); case CRON -> nextCron(expr, afterInTaskZone); - default -> throw invalidSchedule("Unsupported schedule type: " + type); }; return nextInTaskZone.withZoneSameInstant(storageZone).toLocalDateTime(); } @@ -65,11 +57,12 @@ public String normalizeTimezone(String timezone) { } } - public String normalizeType(String type) { - if (type == null || type.isBlank()) { - throw invalidSchedule("scheduleType cannot be blank."); + public ScheduledAgentScheduleType normalizeType(String type) { + try { + return ScheduledAgentScheduleType.from(type); + } catch (IllegalArgumentException e) { + throw invalidSchedule(e.getMessage()); } - return type.trim().toUpperCase(); } private ZonedDateTime nextDaily(String expr, ZonedDateTime after) { diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java index c69fb9f..2b4f673 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java @@ -23,6 +23,8 @@ import io.github.malonetalk.entity.ScheduledAgentTask; import io.github.malonetalk.entity.ScheduledAgentTaskRun; import io.github.malonetalk.enums.ChatStreamEventType; +import io.github.malonetalk.enums.ScheduledAgentSessionMode; +import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; import io.github.malonetalk.mapper.ScheduledAgentTaskRunMapper; import java.time.Duration; @@ -31,6 +33,7 @@ import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Slf4j @@ -38,11 +41,6 @@ @RequiredArgsConstructor public class ScheduledAgentTaskRunner { - private static final Duration LOCK_DURATION = Duration.ofMinutes(30); - private static final String RUNNING = "RUNNING"; - private static final String SUCCESS = "SUCCESS"; - private static final String FAILED = "FAILED"; - private static final String NEEDS_USER = "NEEDS_USER"; private static final int SUMMARY_LIMIT = 2000; private final AgentService agentService; @@ -50,13 +48,20 @@ public class ScheduledAgentTaskRunner { private final ScheduledAgentTaskRunMapper runMapper; private final ScheduledAgentScheduleCalculator scheduleCalculator; - public void run(Integer taskId, boolean force) { + @Value("${data-agent.schedule.lock-duration:PT30M}") + private Duration lockDuration = Duration.ofMinutes(30); + + public ClaimedRun claim(Integer taskId, boolean force) { LocalDateTime startedAt = LocalDateTime.now(); String lockOwner = UUID.randomUUID().toString(); if (taskMapper.lockForRun( - taskId, startedAt, startedAt.plus(LOCK_DURATION), lockOwner, force) + taskId, + startedAt, + startedAt.plus(effectiveLockDuration()), + lockOwner, + force) == 0) { - return; + return null; } ScheduledAgentTask task = taskMapper.selectById(taskId); @@ -65,22 +70,29 @@ public void run(Integer taskId, boolean force) { if (taskMapper.markRunStarted(taskId, lockOwner, run.getId()) == 0) { runMapper.finish( run.getId(), - FAILED, + ScheduledAgentTaskStatus.FAILED.name(), null, null, "Scheduled task lock was lost before the run started.", LocalDateTime.now()); - return; + return null; } + return new ClaimedRun(task, run, lockOwner, force); + } - String status = SUCCESS; + public void run(ClaimedRun claimedRun) { + ScheduledAgentTaskStatus status = ScheduledAgentTaskStatus.SUCCESS; Integer reportId = null; String outputSummary = null; String errorMessage = null; try { List events = agentService - .chatStream(sessionId, buildPrompt(task), null) + .chatStream( + claimedRun.run().getSessionId(), + buildPrompt(claimedRun.task()), + null, + false) .collectList() .block(); status = resolveStatus(events); @@ -88,20 +100,44 @@ public void run(Integer taskId, boolean force) { outputSummary = limitText(extractOutput(events)); errorMessage = limitText(extractError(events)); } catch (Exception e) { - status = FAILED; + status = ScheduledAgentTaskStatus.FAILED; errorMessage = limitText(e.getMessage()); - log.error("Scheduled agent task failed: taskId={}", taskId, e); + log.error("Scheduled agent task failed: taskId={}", claimedRun.task().getId(), e); } finally { - finish(task, run, lockOwner, force, status, reportId, outputSummary, errorMessage); + finish( + claimedRun.task(), + claimedRun.run(), + claimedRun.lockOwner(), + claimedRun.force(), + status, + reportId, + outputSummary, + errorMessage); } } + public void reject(ClaimedRun claimedRun, String reason) { + LocalDateTime finishedAt = LocalDateTime.now(); + runMapper.finish( + claimedRun.run().getId(), + ScheduledAgentTaskStatus.FAILED.name(), + null, + null, + reason, + finishedAt); + taskMapper.releaseClaim( + claimedRun.task().getId(), + claimedRun.lockOwner(), + claimedRun.run().getId(), + finishedAt); + } + private ScheduledAgentTaskRun startRun( Integer taskId, String sessionId, LocalDateTime startedAt) { ScheduledAgentTaskRun run = new ScheduledAgentTaskRun(); run.setTaskId(taskId); run.setSessionId(sessionId); - run.setStatus(RUNNING); + run.setStatus(ScheduledAgentTaskStatus.RUNNING.name()); run.setStartedAt(startedAt); runMapper.insert(run); return run; @@ -112,12 +148,13 @@ private void finish( ScheduledAgentTaskRun run, String lockOwner, boolean force, - String status, + ScheduledAgentTaskStatus status, Integer reportId, String outputSummary, String errorMessage) { LocalDateTime finishedAt = LocalDateTime.now(); - runMapper.finish(run.getId(), status, reportId, outputSummary, errorMessage, finishedAt); + runMapper.finish( + run.getId(), status.name(), reportId, outputSummary, errorMessage, finishedAt); LocalDateTime nextRunAt = force && task.getNextRunAt().isAfter(finishedAt) ? task.getNextRunAt() @@ -133,7 +170,7 @@ private void finish( run.getId(), nextRunAt, finishedAt, - status, + status.name(), errorMessage); if (updated == 0) { log.warn( @@ -144,7 +181,7 @@ private void finish( } private String resolveSessionId(ScheduledAgentTask task) { - if (ScheduledAgentTaskServiceImpl.FIXED_SESSION.equals(task.getSessionMode())) { + if (ScheduledAgentSessionMode.FIXED_SESSION.name().equals(task.getSessionMode())) { return task.getSessionId(); } return "scheduled-task-" + task.getId() + "-" + UUID.randomUUID(); @@ -154,17 +191,17 @@ private String buildPrompt(ScheduledAgentTask task) { return task.getPrompt() + "\n\n定时任务执行要求:如果信息不足或需要用户确认,请直接说明无法完成,不要调用 ask_user 反问用户。"; } - private String resolveStatus(List events) { + private ScheduledAgentTaskStatus resolveStatus(List events) { if (events == null) { - return FAILED; + return ScheduledAgentTaskStatus.FAILED; } if (events.stream().anyMatch(event -> event.type() == ChatStreamEventType.ERROR)) { - return FAILED; + return ScheduledAgentTaskStatus.FAILED; } if (events.stream().anyMatch(event -> event.type() == ChatStreamEventType.QUESTION)) { - return NEEDS_USER; + return ScheduledAgentTaskStatus.NEEDS_USER; } - return SUCCESS; + return ScheduledAgentTaskStatus.SUCCESS; } private Integer extractReportId(List events) { @@ -218,4 +255,14 @@ private String limitText(String text) { } return text.substring(0, SUMMARY_LIMIT); } + + private Duration effectiveLockDuration() { + if (lockDuration.isZero() || lockDuration.isNegative()) { + throw new IllegalStateException("data-agent.schedule.lock-duration must be positive."); + } + return lockDuration; + } + + public record ClaimedRun( + ScheduledAgentTask task, ScheduledAgentTaskRun run, String lockOwner, boolean force) {} } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java deleted file mode 100644 index e488d20..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskScheduler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.service; - -import io.github.malonetalk.entity.ScheduledAgentTask; -import java.util.List; -import java.util.Locale; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Service -public class ScheduledAgentTaskScheduler { - - private final ScheduledAgentTaskSchedulerStrategy selectedStrategy; - - public ScheduledAgentTaskScheduler( - List strategies, - @Value("${data-agent.schedule.scheduler-type:db-polling}") String schedulerType) { - this.selectedStrategy = select(strategies, schedulerType); - } - - public void sync(ScheduledAgentTask task) { - selectedStrategy.sync(task); - } - - public void unschedule(Integer taskId) { - selectedStrategy.unschedule(taskId); - } - - public void runNow(Integer taskId) { - selectedStrategy.runNow(taskId); - } - - private ScheduledAgentTaskSchedulerStrategy select( - List strategies, String schedulerType) { - String selectedType = normalize(schedulerType); - return strategies.stream() - .filter(strategy -> selectedType.equals(normalize(strategy.type()))) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - "Unsupported scheduled task scheduler type: " - + schedulerType)); - } - - private String normalize(String value) { - return value == null ? "" : value.trim().toLowerCase(Locale.ROOT); - } -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java index 2a21650..7ae383c 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java @@ -23,6 +23,9 @@ import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; import io.github.malonetalk.entity.ScheduledAgentTask; import io.github.malonetalk.entity.ScheduledAgentTaskRun; +import io.github.malonetalk.enums.ScheduledAgentScheduleType; +import io.github.malonetalk.enums.ScheduledAgentSessionMode; +import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import io.github.malonetalk.exception.BusinessException; import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; import io.github.malonetalk.mapper.ScheduledAgentTaskRunMapper; @@ -37,13 +40,10 @@ public class ScheduledAgentTaskServiceImpl implements ScheduledAgentTaskService { public static final String DEFAULT_TIMEZONE = "Asia/Shanghai"; - public static final String NEW_EACH_RUN = "NEW_EACH_RUN"; - public static final String FIXED_SESSION = "FIXED_SESSION"; private final ScheduledAgentTaskMapper taskMapper; private final ScheduledAgentTaskRunMapper runMapper; private final ScheduledAgentScheduleCalculator scheduleCalculator; - private final ScheduledAgentTaskScheduler taskScheduler; @Override public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { @@ -56,7 +56,6 @@ public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { task.setCreateTime(now); task.setUpdateTime(now); taskMapper.insert(task); - taskScheduler.sync(task); return toResponse(task); } @@ -73,7 +72,6 @@ public ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest r task.setUpdateTime(LocalDateTime.now()); taskMapper.update(task); ScheduledAgentTask saved = taskMapper.selectById(id); - taskScheduler.sync(saved); return toResponse(saved); } @@ -83,7 +81,6 @@ public void delete(Integer id) { if (taskMapper.deleteById(id) == 0) { throw notFound(id); } - taskScheduler.unschedule(id); } @Override @@ -98,10 +95,8 @@ public List listAll() { @Override public void updateEnabled(Integer id, boolean enabled) { - ScheduledAgentTask task = getTask(id); + getTask(id); taskMapper.updateEnabled(id, enabled, LocalDateTime.now()); - task.setEnabled(enabled); - taskScheduler.sync(task); } @Override @@ -121,36 +116,29 @@ private ScheduledAgentTask buildTask( : request.timezone().trim(); timezone = scheduleCalculator.normalizeTimezone(timezone); - String sessionMode = normalizeSessionMode(request.sessionMode()); + ScheduledAgentSessionMode sessionMode = normalizeSessionMode(request.sessionMode()); String sessionId = normalizeSessionId(sessionMode, request.sessionId()); + ScheduledAgentScheduleType scheduleType = request.scheduleType(); task.setName(RequestAssert.requireNotBlank(request.name(), "name cannot be blank.")); task.setPrompt(RequestAssert.requireNotBlank(request.prompt(), "prompt cannot be blank.")); - task.setScheduleType(scheduleCalculator.normalizeType(request.scheduleType())); + task.setScheduleType(scheduleType.name()); task.setScheduleExpr( RequestAssert.requireNotBlank( request.scheduleExpr(), "scheduleExpr cannot be blank.")); task.setTimezone(timezone); task.setEnabled(request.enabled() == null || request.enabled()); - task.setSessionMode(sessionMode); + task.setSessionMode(sessionMode.name()); task.setSessionId(sessionId); return task; } - private String normalizeSessionMode(String sessionMode) { - if (sessionMode == null || sessionMode.isBlank()) { - return NEW_EACH_RUN; - } - String normalized = sessionMode.trim().toUpperCase(); - if (!NEW_EACH_RUN.equals(normalized) && !FIXED_SESSION.equals(normalized)) { - throw BusinessException.of( - ErrorCode.BAD_REQUEST, "Unsupported sessionMode: " + sessionMode); - } - return normalized; + private ScheduledAgentSessionMode normalizeSessionMode(ScheduledAgentSessionMode sessionMode) { + return sessionMode == null ? ScheduledAgentSessionMode.NEW_EACH_RUN : sessionMode; } - private String normalizeSessionId(String sessionMode, String sessionId) { - if (!FIXED_SESSION.equals(sessionMode)) { + private String normalizeSessionId(ScheduledAgentSessionMode sessionMode, String sessionId) { + if (sessionMode != ScheduledAgentSessionMode.FIXED_SESSION) { return null; } return RequestAssert.requireNotBlank( @@ -176,16 +164,16 @@ private ScheduledAgentTaskResponse toResponse(ScheduledAgentTask task) { task.getId(), task.getName(), task.getPrompt(), - task.getScheduleType(), + ScheduledAgentScheduleType.from(task.getScheduleType()), task.getScheduleExpr(), task.getTimezone(), task.getEnabled(), task.getRunning(), - task.getSessionMode(), + ScheduledAgentSessionMode.fromOrDefault(task.getSessionMode()), task.getSessionId(), task.getNextRunAt(), task.getLastRunAt(), - task.getLastStatus(), + toStatus(task.getLastStatus()), task.getLastError(), task.getCreateTime(), task.getUpdateTime()); @@ -196,11 +184,15 @@ private ScheduledAgentTaskRunResponse toRunResponse(ScheduledAgentTaskRun run) { run.getId(), run.getTaskId(), run.getSessionId(), - run.getStatus(), + toStatus(run.getStatus()), run.getReportId(), run.getOutputSummary(), run.getErrorMessage(), run.getStartedAt(), run.getFinishedAt()); } + + private ScheduledAgentTaskStatus toStatus(String status) { + return status == null ? null : ScheduledAgentTaskStatus.valueOf(status); + } } diff --git a/data-agent-backend/src/main/resources/application.properties b/data-agent-backend/src/main/resources/application.properties index 02d19fb..8350f8c 100644 --- a/data-agent-backend/src/main/resources/application.properties +++ b/data-agent-backend/src/main/resources/application.properties @@ -20,6 +20,12 @@ mybatis.type-aliases-package=io.github.malonetalk.entity mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl +# Scheduled Agent Task Configuration +data-agent.schedule.lock-duration=${SCHEDULE_LOCK_DURATION:PT30M} +data-agent.schedule.executor.core-size=${SCHEDULE_EXECUTOR_CORE_SIZE:3} +data-agent.schedule.executor.max-size=${SCHEDULE_EXECUTOR_MAX_SIZE:3} +data-agent.schedule.executor.queue-capacity=${SCHEDULE_EXECUTOR_QUEUE_CAPACITY:20} + # Model Configuration io.github.malonetalk.model.provider=dashscope io.github.malonetalk.model.name=qwen3-max diff --git a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml index e53f1a8..379e390 100644 --- a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml @@ -120,4 +120,16 @@ AND lock_owner = #{lockOwner} AND current_run_id = #{currentRunId} + + + UPDATE scheduled_agent_task + SET running = 0, + lock_until = NULL, + lock_owner = NULL, + current_run_id = NULL, + update_time = #{updateTime} + WHERE id = #{id} + AND lock_owner = #{lockOwner} + AND current_run_id = #{currentRunId} + diff --git a/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue index 9223a21..8481fde 100644 --- a/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue +++ b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue @@ -205,8 +205,12 @@ async function runNow(row: ScheduledTaskResponse) { runningTaskId.value = row.id; try { - await runScheduledTask(row.id); - ElMessage.success('已提交运行'); + const response = await runScheduledTask(row.id); + if (response.data.data) { + ElMessage.success('已提交运行'); + } else { + ElMessage.warning('任务正在运行,未重复提交'); + } await loadTasks(); } finally { runningTaskId.value = null; From 3d78c5e65d0aee09d80631f41bf5a2c4f02fe75f Mon Sep 17 00:00:00 2001 From: mengnankkkk Date: Fri, 7 Aug 2026 21:45:53 +0800 Subject: [PATCH 05/19] fix:remove something --- .../ScheduledAgentTaskController.java | 34 +--- .../dto/ScheduledAgentTaskRequest.java | 5 +- .../dto/ScheduledAgentTaskResponse.java | 6 - .../dto/ScheduledAgentTaskRunResponse.java | 32 ---- .../malonetalk/entity/ScheduledAgentTask.java | 8 - .../entity/ScheduledAgentTaskRun.java | 35 ---- .../enums/ScheduledAgentScheduleType.java | 15 +- .../enums/ScheduledAgentSessionMode.java | 36 ----- .../enums/ScheduledAgentTaskStatus.java | 1 - .../mapper/ScheduledAgentTaskMapper.java | 17 +- .../mapper/ScheduledAgentTaskRunMapper.java | 41 ----- ...asePollingScheduledAgentTaskScheduler.java | 14 +- .../ScheduledAgentScheduleCalculator.java | 10 +- .../service/ScheduledAgentTaskRunner.java | 153 ++---------------- .../service/ScheduledAgentTaskService.java | 40 ----- .../ScheduledAgentTaskServiceImpl.java | 68 ++------ .../mapper/ScheduledAgentTaskMapper.xml | 50 +----- .../mapper/ScheduledAgentTaskRunMapper.xml | 42 ----- data-agent-frontend/src/api/scheduledTask.ts | 35 +--- .../scheduled-task/ScheduledTaskManage.vue | 87 +--------- sql/data_source.sql | 25 +-- 21 files changed, 52 insertions(+), 702 deletions(-) delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java delete mode 100644 data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java index 42b8e8f..e7b193e 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java @@ -20,9 +20,7 @@ import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.ScheduledAgentTaskRequest; import io.github.malonetalk.dto.ScheduledAgentTaskResponse; -import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; -import io.github.malonetalk.service.DatabasePollingScheduledAgentTaskScheduler; -import io.github.malonetalk.service.ScheduledAgentTaskService; +import io.github.malonetalk.service.ScheduledAgentTaskServiceImpl; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import java.util.List; @@ -35,7 +33,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Validated @@ -44,8 +41,7 @@ @RequestMapping("/api/scheduled-agent-tasks") public class ScheduledAgentTaskController { - private final ScheduledAgentTaskService taskService; - private final DatabasePollingScheduledAgentTaskScheduler taskScheduler; + private final ScheduledAgentTaskServiceImpl taskService; @PostMapping public Result create( @@ -78,31 +74,17 @@ public Result> listAll() { return Result.success(taskService.listAll()); } - @PostMapping("/{id}/enable") - public Result enable( - @PathVariable @Positive(message = "id must be positive.") Integer id) { - taskService.updateEnabled(id, true); - return Result.success(true); - } - - @PostMapping("/{id}/disable") - public Result disable( - @PathVariable @Positive(message = "id must be positive.") Integer id) { - taskService.updateEnabled(id, false); + @PutMapping("/{id}/enabled/{enabled}") + public Result updateEnabled( + @PathVariable @Positive(message = "id must be positive.") Integer id, + @PathVariable boolean enabled) { + taskService.updateEnabled(id, enabled); return Result.success(true); } @PostMapping("/{id}/run") public Result runNow( @PathVariable @Positive(message = "id must be positive.") Integer id) { - taskService.getById(id); - return Result.success(taskScheduler.runNow(id)); - } - - @GetMapping("/{id}/runs") - public Result> listRuns( - @PathVariable @Positive(message = "id must be positive.") Integer id, - @RequestParam(defaultValue = "20") int limit) { - return Result.success(taskService.listRuns(id, limit)); + return Result.success(taskService.runNow(id)); } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java index 31f9753..e61dd89 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java @@ -18,7 +18,6 @@ package io.github.malonetalk.dto; import io.github.malonetalk.enums.ScheduledAgentScheduleType; -import io.github.malonetalk.enums.ScheduledAgentSessionMode; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -28,6 +27,4 @@ public record ScheduledAgentTaskRequest( @NotNull(message = "scheduleType cannot be null.") ScheduledAgentScheduleType scheduleType, @NotBlank(message = "scheduleExpr cannot be blank.") String scheduleExpr, String timezone, - Boolean enabled, - ScheduledAgentSessionMode sessionMode, - String sessionId) {} + Boolean enabled) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java index 0202bbf..72674f8 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java @@ -18,7 +18,6 @@ package io.github.malonetalk.dto; import io.github.malonetalk.enums.ScheduledAgentScheduleType; -import io.github.malonetalk.enums.ScheduledAgentSessionMode; import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import java.time.LocalDateTime; @@ -30,12 +29,7 @@ public record ScheduledAgentTaskResponse( String scheduleExpr, String timezone, Boolean enabled, - Boolean running, - ScheduledAgentSessionMode sessionMode, - String sessionId, LocalDateTime nextRunAt, - LocalDateTime lastRunAt, ScheduledAgentTaskStatus lastStatus, - String lastError, LocalDateTime createTime, LocalDateTime updateTime) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java deleted file mode 100644 index bde47d6..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRunResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.dto; - -import io.github.malonetalk.enums.ScheduledAgentTaskStatus; -import java.time.LocalDateTime; - -public record ScheduledAgentTaskRunResponse( - Integer id, - Integer taskId, - String sessionId, - ScheduledAgentTaskStatus status, - Integer reportId, - String outputSummary, - String errorMessage, - LocalDateTime startedAt, - LocalDateTime finishedAt) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java index fe58687..407a830 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java @@ -30,16 +30,8 @@ public class ScheduledAgentTask { private String scheduleExpr; private String timezone; private Boolean enabled; - private Boolean running; - private LocalDateTime lockUntil; - private String lockOwner; - private Integer currentRunId; - private String sessionMode; - private String sessionId; private LocalDateTime nextRunAt; - private LocalDateTime lastRunAt; private String lastStatus; - private String lastError; private LocalDateTime createTime; private LocalDateTime updateTime; } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java deleted file mode 100644 index 79f6466..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTaskRun.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.entity; - -import java.time.LocalDateTime; -import lombok.Data; - -@Data -public class ScheduledAgentTaskRun { - - private Integer id; - private Integer taskId; - private String sessionId; - private String status; - private Integer reportId; - private String outputSummary; - private String errorMessage; - private LocalDateTime startedAt; - private LocalDateTime finishedAt; -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java index 3d2a60f..d41562a 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentScheduleType.java @@ -17,21 +17,8 @@ */ package io.github.malonetalk.enums; -import java.util.Locale; - public enum ScheduledAgentScheduleType { DAILY, INTERVAL, - CRON; - - public static ScheduledAgentScheduleType from(String value) { - if (value == null || value.isBlank()) { - throw new IllegalArgumentException("scheduleType cannot be blank."); - } - try { - return valueOf(value.trim().toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Unsupported schedule type: " + value, e); - } - } + CRON } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java deleted file mode 100644 index e15266c..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentSessionMode.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.enums; - -import java.util.Locale; - -public enum ScheduledAgentSessionMode { - NEW_EACH_RUN, - FIXED_SESSION; - - public static ScheduledAgentSessionMode fromOrDefault(String value) { - if (value == null || value.isBlank()) { - return NEW_EACH_RUN; - } - try { - return valueOf(value.trim().toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Unsupported sessionMode: " + value, e); - } - } -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java index cc81fbd..50ff72e 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java @@ -18,7 +18,6 @@ package io.github.malonetalk.enums; public enum ScheduledAgentTaskStatus { - RUNNING, SUCCESS, FAILED, NEEDS_USER diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java index 248836e..ac5c9c4 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java @@ -51,23 +51,10 @@ int lockForRun( @Param("lockOwner") String lockOwner, @Param("force") boolean force); - int markRunStarted( - @Param("id") Integer id, - @Param("lockOwner") String lockOwner, - @Param("currentRunId") Integer currentRunId); - int finishRun( @Param("id") Integer id, @Param("lockOwner") String lockOwner, - @Param("currentRunId") Integer currentRunId, @Param("nextRunAt") LocalDateTime nextRunAt, - @Param("lastRunAt") LocalDateTime lastRunAt, - @Param("lastStatus") String lastStatus, - @Param("lastError") String lastError); - - int releaseClaim( - @Param("id") Integer id, - @Param("lockOwner") String lockOwner, - @Param("currentRunId") Integer currentRunId, - @Param("updateTime") LocalDateTime updateTime); + @Param("finishedAt") LocalDateTime finishedAt, + @Param("lastStatus") String lastStatus); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java deleted file mode 100644 index 8f9f0d0..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskRunMapper.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.mapper; - -import io.github.malonetalk.entity.ScheduledAgentTaskRun; -import java.time.LocalDateTime; -import java.util.List; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -@Mapper -public interface ScheduledAgentTaskRunMapper { - - int insert(ScheduledAgentTaskRun run); - - int finish( - @Param("id") Integer id, - @Param("status") String status, - @Param("reportId") Integer reportId, - @Param("outputSummary") String outputSummary, - @Param("errorMessage") String errorMessage, - @Param("finishedAt") LocalDateTime finishedAt); - - List selectByTaskId( - @Param("taskId") Integer taskId, @Param("limit") int limit); -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java index bfc53c5..5b58e83 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java @@ -63,9 +63,6 @@ public void initExecutor() { @Scheduled(fixedDelayString = "${data-agent.schedule.dispatch-delay-ms:10000}") public void dispatchDueTasks() { for (var task : taskMapper.findDueTasks(LocalDateTime.now(), BATCH_SIZE)) { - if (!hasCapacity()) { - return; - } ScheduledAgentTaskRunner.ClaimedRun claimedRun = taskRunner.claim(task.getId(), false); if (claimedRun != null && !execute(claimedRun)) { return; @@ -74,9 +71,6 @@ public void dispatchDueTasks() { } public boolean runNow(Integer taskId) { - if (!hasCapacity()) { - return false; - } ScheduledAgentTaskRunner.ClaimedRun claimedRun = taskRunner.claim(taskId, true); if (claimedRun == null) { return false; @@ -89,18 +83,12 @@ private boolean execute(ScheduledAgentTaskRunner.ClaimedRun claimedRun) { executor.execute(() -> taskRunner.run(claimedRun)); return true; } catch (TaskRejectedException e) { - taskRunner.reject(claimedRun, "Scheduled task executor rejected the run."); + taskRunner.reject(claimedRun); log.warn("Scheduled agent task executor rejected taskId={}", claimedRun.task().getId()); return false; } } - private boolean hasCapacity() { - ThreadPoolExecutor pool = executor.getThreadPoolExecutor(); - return pool.getActiveCount() < pool.getMaximumPoolSize() - || pool.getQueue().remainingCapacity() > 0; - } - @PreDestroy public void shutdown() { executor.shutdown(); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java index 6dfe4dd..d3a2d71 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java @@ -38,7 +38,7 @@ public LocalDateTime nextRunAfter( ZoneId storageZone = ZoneId.systemDefault(); ZonedDateTime afterInTaskZone = after.atZone(storageZone).withZoneSameInstant(taskZone); ZonedDateTime nextInTaskZone = - switch (normalizeType(type)) { + switch (ScheduledAgentScheduleType.valueOf(type)) { case DAILY -> nextDaily(expr, afterInTaskZone); case INTERVAL -> afterInTaskZone.plus(parsePositiveDuration(expr)); case CRON -> nextCron(expr, afterInTaskZone); @@ -57,14 +57,6 @@ public String normalizeTimezone(String timezone) { } } - public ScheduledAgentScheduleType normalizeType(String type) { - try { - return ScheduledAgentScheduleType.from(type); - } catch (IllegalArgumentException e) { - throw invalidSchedule(e.getMessage()); - } - } - private ZonedDateTime nextDaily(String expr, ZonedDateTime after) { LocalTime time = parseDailyTime(expr); ZonedDateTime next = after.toLocalDate().atTime(time).atZone(after.getZone()); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java index 2b4f673..7a6f621 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java @@ -18,15 +18,11 @@ package io.github.malonetalk.service; import io.github.malonetalk.agent.AgentService; -import io.github.malonetalk.agent.tools.ToolCallConstants; import io.github.malonetalk.dto.ChatStreamEvent; import io.github.malonetalk.entity.ScheduledAgentTask; -import io.github.malonetalk.entity.ScheduledAgentTaskRun; import io.github.malonetalk.enums.ChatStreamEventType; -import io.github.malonetalk.enums.ScheduledAgentSessionMode; import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; -import io.github.malonetalk.mapper.ScheduledAgentTaskRunMapper; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; @@ -41,11 +37,8 @@ @RequiredArgsConstructor public class ScheduledAgentTaskRunner { - private static final int SUMMARY_LIMIT = 2000; - private final AgentService agentService; private final ScheduledAgentTaskMapper taskMapper; - private final ScheduledAgentTaskRunMapper runMapper; private final ScheduledAgentScheduleCalculator scheduleCalculator; @Value("${data-agent.schedule.lock-duration:PT30M}") @@ -65,96 +58,45 @@ public ClaimedRun claim(Integer taskId, boolean force) { } ScheduledAgentTask task = taskMapper.selectById(taskId); - String sessionId = resolveSessionId(task); - ScheduledAgentTaskRun run = startRun(taskId, sessionId, startedAt); - if (taskMapper.markRunStarted(taskId, lockOwner, run.getId()) == 0) { - runMapper.finish( - run.getId(), - ScheduledAgentTaskStatus.FAILED.name(), - null, - null, - "Scheduled task lock was lost before the run started.", - LocalDateTime.now()); - return null; - } - return new ClaimedRun(task, run, lockOwner, force); + String sessionId = "scheduled-task-" + task.getId() + "-" + UUID.randomUUID(); + return new ClaimedRun(task, sessionId, lockOwner, force); } public void run(ClaimedRun claimedRun) { ScheduledAgentTaskStatus status = ScheduledAgentTaskStatus.SUCCESS; - Integer reportId = null; - String outputSummary = null; - String errorMessage = null; try { List events = agentService .chatStream( - claimedRun.run().getSessionId(), + claimedRun.sessionId(), buildPrompt(claimedRun.task()), null, false) .collectList() .block(); status = resolveStatus(events); - reportId = extractReportId(events); - outputSummary = limitText(extractOutput(events)); - errorMessage = limitText(extractError(events)); } catch (Exception e) { status = ScheduledAgentTaskStatus.FAILED; - errorMessage = limitText(e.getMessage()); log.error("Scheduled agent task failed: taskId={}", claimedRun.task().getId(), e); } finally { - finish( - claimedRun.task(), - claimedRun.run(), - claimedRun.lockOwner(), - claimedRun.force(), - status, - reportId, - outputSummary, - errorMessage); + finish(claimedRun.task(), claimedRun.lockOwner(), claimedRun.force(), status); } } - public void reject(ClaimedRun claimedRun, String reason) { - LocalDateTime finishedAt = LocalDateTime.now(); - runMapper.finish( - claimedRun.run().getId(), - ScheduledAgentTaskStatus.FAILED.name(), - null, - null, - reason, - finishedAt); - taskMapper.releaseClaim( - claimedRun.task().getId(), + public void reject(ClaimedRun claimedRun) { + finish( + claimedRun.task(), claimedRun.lockOwner(), - claimedRun.run().getId(), - finishedAt); - } - - private ScheduledAgentTaskRun startRun( - Integer taskId, String sessionId, LocalDateTime startedAt) { - ScheduledAgentTaskRun run = new ScheduledAgentTaskRun(); - run.setTaskId(taskId); - run.setSessionId(sessionId); - run.setStatus(ScheduledAgentTaskStatus.RUNNING.name()); - run.setStartedAt(startedAt); - runMapper.insert(run); - return run; + claimedRun.force(), + ScheduledAgentTaskStatus.FAILED); } private void finish( ScheduledAgentTask task, - ScheduledAgentTaskRun run, String lockOwner, boolean force, - ScheduledAgentTaskStatus status, - Integer reportId, - String outputSummary, - String errorMessage) { + ScheduledAgentTaskStatus status) { LocalDateTime finishedAt = LocalDateTime.now(); - runMapper.finish( - run.getId(), status.name(), reportId, outputSummary, errorMessage, finishedAt); LocalDateTime nextRunAt = force && task.getNextRunAt().isAfter(finishedAt) ? task.getNextRunAt() @@ -164,29 +106,12 @@ private void finish( finishedAt, task.getTimezone()); int updated = - taskMapper.finishRun( - task.getId(), - lockOwner, - run.getId(), - nextRunAt, - finishedAt, - status.name(), - errorMessage); + taskMapper.finishRun(task.getId(), lockOwner, nextRunAt, finishedAt, status.name()); if (updated == 0) { - log.warn( - "Scheduled agent task lock changed before finish: taskId={}, runId={}", - task.getId(), - run.getId()); + log.warn("Scheduled agent task lock changed before finish: taskId={}", task.getId()); } } - private String resolveSessionId(ScheduledAgentTask task) { - if (ScheduledAgentSessionMode.FIXED_SESSION.name().equals(task.getSessionMode())) { - return task.getSessionId(); - } - return "scheduled-task-" + task.getId() + "-" + UUID.randomUUID(); - } - private String buildPrompt(ScheduledAgentTask task) { return task.getPrompt() + "\n\n定时任务执行要求:如果信息不足或需要用户确认,请直接说明无法完成,不要调用 ask_user 反问用户。"; } @@ -204,58 +129,6 @@ private ScheduledAgentTaskStatus resolveStatus(List events) { return ScheduledAgentTaskStatus.SUCCESS; } - private Integer extractReportId(List events) { - if (events == null) { - return null; - } - return events.stream() - .filter(event -> event.type() == ChatStreamEventType.REPORT) - .map(ChatStreamEvent::content) - .filter( - content -> - content != null - && content.startsWith(ToolCallConstants.SUCCESS_PREFIX)) - .map(content -> content.substring(ToolCallConstants.SUCCESS_PREFIX.length()).trim()) - .map(Integer::valueOf) - .findFirst() - .orElse(null); - } - - private String extractOutput(List events) { - if (events == null) { - return null; - } - for (int i = events.size() - 1; i >= 0; i--) { - ChatStreamEvent event = events.get(i); - if ((event.type() == ChatStreamEventType.SUMMARY - || event.type() == ChatStreamEventType.TEXT) - && event.content() != null - && !event.content().isBlank()) { - return event.content(); - } - } - return null; - } - - private String extractError(List events) { - if (events == null) { - return "Agent stream returned no events."; - } - return events.stream() - .filter(event -> event.type() == ChatStreamEventType.ERROR) - .map(ChatStreamEvent::content) - .filter(content -> content != null && !content.isBlank()) - .findFirst() - .orElse(null); - } - - private String limitText(String text) { - if (text == null || text.length() <= SUMMARY_LIMIT) { - return text; - } - return text.substring(0, SUMMARY_LIMIT); - } - private Duration effectiveLockDuration() { if (lockDuration.isZero() || lockDuration.isNegative()) { throw new IllegalStateException("data-agent.schedule.lock-duration must be positive."); @@ -264,5 +137,5 @@ private Duration effectiveLockDuration() { } public record ClaimedRun( - ScheduledAgentTask task, ScheduledAgentTaskRun run, String lockOwner, boolean force) {} + ScheduledAgentTask task, String sessionId, String lockOwner, boolean force) {} } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java deleted file mode 100644 index eb7cb79..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskService.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.service; - -import io.github.malonetalk.dto.ScheduledAgentTaskRequest; -import io.github.malonetalk.dto.ScheduledAgentTaskResponse; -import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; -import java.util.List; - -public interface ScheduledAgentTaskService { - - ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request); - - ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest request); - - void delete(Integer id); - - ScheduledAgentTaskResponse getById(Integer id); - - List listAll(); - - void updateEnabled(Integer id, boolean enabled); - - List listRuns(Integer taskId, int limit); -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java index 7ae383c..69eac6e 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java @@ -20,15 +20,11 @@ import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.dto.ScheduledAgentTaskRequest; import io.github.malonetalk.dto.ScheduledAgentTaskResponse; -import io.github.malonetalk.dto.ScheduledAgentTaskRunResponse; import io.github.malonetalk.entity.ScheduledAgentTask; -import io.github.malonetalk.entity.ScheduledAgentTaskRun; import io.github.malonetalk.enums.ScheduledAgentScheduleType; -import io.github.malonetalk.enums.ScheduledAgentSessionMode; import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import io.github.malonetalk.exception.BusinessException; import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; -import io.github.malonetalk.mapper.ScheduledAgentTaskRunMapper; import io.github.malonetalk.utils.RequestAssert; import java.time.LocalDateTime; import java.util.List; @@ -37,19 +33,17 @@ @Service @RequiredArgsConstructor -public class ScheduledAgentTaskServiceImpl implements ScheduledAgentTaskService { +public class ScheduledAgentTaskServiceImpl { - public static final String DEFAULT_TIMEZONE = "Asia/Shanghai"; + private static final String DEFAULT_TIMEZONE = "Asia/Shanghai"; private final ScheduledAgentTaskMapper taskMapper; - private final ScheduledAgentTaskRunMapper runMapper; private final ScheduledAgentScheduleCalculator scheduleCalculator; + private final DatabasePollingScheduledAgentTaskScheduler taskScheduler; - @Override public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { ScheduledAgentTask task = buildTask(new ScheduledAgentTask(), request); LocalDateTime now = LocalDateTime.now(); - task.setRunning(false); task.setNextRunAt( scheduleCalculator.nextRunAfter( task.getScheduleType(), task.getScheduleExpr(), now, task.getTimezone())); @@ -59,7 +53,6 @@ public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { return toResponse(task); } - @Override public ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest request) { ScheduledAgentTask existing = getTask(id); ScheduledAgentTask task = buildTask(existing, request); @@ -75,7 +68,6 @@ public ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest r return toResponse(saved); } - @Override public void delete(Integer id) { RequestAssert.requireNonNegative(id, "id must be non-negative."); if (taskMapper.deleteById(id) == 0) { @@ -83,29 +75,23 @@ public void delete(Integer id) { } } - @Override public ScheduledAgentTaskResponse getById(Integer id) { return toResponse(getTask(id)); } - @Override public List listAll() { return taskMapper.selectAll().stream().map(this::toResponse).toList(); } - @Override public void updateEnabled(Integer id, boolean enabled) { - getTask(id); - taskMapper.updateEnabled(id, enabled, LocalDateTime.now()); + RequestAssert.requireNonNegative(id, "id must be non-negative."); + if (taskMapper.updateEnabled(id, enabled, LocalDateTime.now()) == 0) { + throw notFound(id); + } } - @Override - public List listRuns(Integer taskId, int limit) { - getTask(taskId); - int resolvedLimit = Math.min(Math.max(limit, 1), 100); - return runMapper.selectByTaskId(taskId, resolvedLimit).stream() - .map(this::toRunResponse) - .toList(); + public boolean runNow(Integer id) { + return taskScheduler.runNow(id); } private ScheduledAgentTask buildTask( @@ -116,8 +102,6 @@ private ScheduledAgentTask buildTask( : request.timezone().trim(); timezone = scheduleCalculator.normalizeTimezone(timezone); - ScheduledAgentSessionMode sessionMode = normalizeSessionMode(request.sessionMode()); - String sessionId = normalizeSessionId(sessionMode, request.sessionId()); ScheduledAgentScheduleType scheduleType = request.scheduleType(); task.setName(RequestAssert.requireNotBlank(request.name(), "name cannot be blank.")); @@ -128,23 +112,9 @@ private ScheduledAgentTask buildTask( request.scheduleExpr(), "scheduleExpr cannot be blank.")); task.setTimezone(timezone); task.setEnabled(request.enabled() == null || request.enabled()); - task.setSessionMode(sessionMode.name()); - task.setSessionId(sessionId); return task; } - private ScheduledAgentSessionMode normalizeSessionMode(ScheduledAgentSessionMode sessionMode) { - return sessionMode == null ? ScheduledAgentSessionMode.NEW_EACH_RUN : sessionMode; - } - - private String normalizeSessionId(ScheduledAgentSessionMode sessionMode, String sessionId) { - if (sessionMode != ScheduledAgentSessionMode.FIXED_SESSION) { - return null; - } - return RequestAssert.requireNotBlank( - sessionId, "sessionId is required when sessionMode is FIXED_SESSION."); - } - private ScheduledAgentTask getTask(Integer id) { RequestAssert.requireNonNegative(id, "id must be non-negative."); ScheduledAgentTask task = taskMapper.selectById(id); @@ -164,34 +134,16 @@ private ScheduledAgentTaskResponse toResponse(ScheduledAgentTask task) { task.getId(), task.getName(), task.getPrompt(), - ScheduledAgentScheduleType.from(task.getScheduleType()), + ScheduledAgentScheduleType.valueOf(task.getScheduleType()), task.getScheduleExpr(), task.getTimezone(), task.getEnabled(), - task.getRunning(), - ScheduledAgentSessionMode.fromOrDefault(task.getSessionMode()), - task.getSessionId(), task.getNextRunAt(), - task.getLastRunAt(), toStatus(task.getLastStatus()), - task.getLastError(), task.getCreateTime(), task.getUpdateTime()); } - private ScheduledAgentTaskRunResponse toRunResponse(ScheduledAgentTaskRun run) { - return new ScheduledAgentTaskRunResponse( - run.getId(), - run.getTaskId(), - run.getSessionId(), - toStatus(run.getStatus()), - run.getReportId(), - run.getOutputSummary(), - run.getErrorMessage(), - run.getStartedAt(), - run.getFinishedAt()); - } - private ScheduledAgentTaskStatus toStatus(String status) { return status == null ? null : ScheduledAgentTaskStatus.valueOf(status); } diff --git a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml index 379e390..af7474e 100644 --- a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml @@ -10,16 +10,8 @@ - - - - - - - - @@ -28,11 +20,11 @@ useGeneratedKeys="true" keyProperty="id"> INSERT INTO scheduled_agent_task ( name, prompt, schedule_type, schedule_expr, timezone, - enabled, running, session_mode, session_id, next_run_at, + enabled, next_run_at, create_time, update_time ) VALUES ( #{name}, #{prompt}, #{scheduleType}, #{scheduleExpr}, #{timezone}, - #{enabled}, #{running}, #{sessionMode}, #{sessionId}, #{nextRunAt}, + #{enabled}, #{nextRunAt}, #{createTime}, #{updateTime} ) @@ -45,8 +37,6 @@ schedule_expr = #{scheduleExpr}, timezone = #{timezone}, enabled = #{enabled}, - session_mode = #{sessionMode}, - session_id = #{sessionId}, next_run_at = #{nextRunAt}, update_time = #{updateTime} WHERE id = #{id} @@ -78,58 +68,32 @@ SELECT * FROM scheduled_agent_task WHERE enabled = 1 AND next_run_at <= #{now} - AND (running = 0 OR lock_until < #{now}) + AND (lock_until IS NULL OR lock_until < #{now}) ORDER BY next_run_at ASC, id ASC LIMIT #{limit} UPDATE scheduled_agent_task - SET running = 1, - lock_until = #{lockUntil}, + SET lock_until = #{lockUntil}, lock_owner = #{lockOwner}, - current_run_id = NULL, update_time = #{now} WHERE id = #{id} - AND (running = 0 OR lock_until < #{now}) + AND (lock_until IS NULL OR lock_until < #{now}) AND enabled = 1 AND next_run_at <= #{now} - - UPDATE scheduled_agent_task - SET current_run_id = #{currentRunId} - WHERE id = #{id} - AND lock_owner = #{lockOwner} - - UPDATE scheduled_agent_task - SET running = 0, - lock_until = NULL, + SET lock_until = NULL, lock_owner = NULL, - current_run_id = NULL, next_run_at = #{nextRunAt}, - last_run_at = #{lastRunAt}, last_status = #{lastStatus}, - last_error = #{lastError}, - update_time = #{lastRunAt} - WHERE id = #{id} - AND lock_owner = #{lockOwner} - AND current_run_id = #{currentRunId} - - - - UPDATE scheduled_agent_task - SET running = 0, - lock_until = NULL, - lock_owner = NULL, - current_run_id = NULL, - update_time = #{updateTime} + update_time = #{finishedAt} WHERE id = #{id} AND lock_owner = #{lockOwner} - AND current_run_id = #{currentRunId} diff --git a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml deleted file mode 100644 index 9254730..0000000 --- a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskRunMapper.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - INSERT INTO scheduled_agent_task_run ( - task_id, session_id, status, started_at - ) VALUES ( - #{taskId}, #{sessionId}, #{status}, #{startedAt} - ) - - - - UPDATE scheduled_agent_task_run - SET status = #{status}, - report_id = #{reportId}, - output_summary = #{outputSummary}, - error_message = #{errorMessage}, - finished_at = #{finishedAt} - WHERE id = #{id} - - - - diff --git a/data-agent-frontend/src/api/scheduledTask.ts b/data-agent-frontend/src/api/scheduledTask.ts index 87a85a7..e6cf4a3 100644 --- a/data-agent-frontend/src/api/scheduledTask.ts +++ b/data-agent-frontend/src/api/scheduledTask.ts @@ -19,7 +19,6 @@ import request from './request'; import type { ApiResponse } from './request'; export type ScheduleType = 'DAILY' | 'INTERVAL' | 'CRON'; -export type SessionMode = 'NEW_EACH_RUN' | 'FIXED_SESSION'; export interface ScheduledTaskRequest { name: string; @@ -28,8 +27,6 @@ export interface ScheduledTaskRequest { scheduleExpr: string; timezone?: string; enabled?: boolean; - sessionMode?: SessionMode; - sessionId?: string; } export interface ScheduledTaskResponse { @@ -40,29 +37,12 @@ export interface ScheduledTaskResponse { scheduleExpr: string; timezone: string; enabled: boolean; - running: boolean; - sessionMode: SessionMode; - sessionId: string | null; nextRunAt: string; - lastRunAt: string | null; lastStatus: string | null; - lastError: string | null; createTime: string; updateTime: string; } -export interface ScheduledTaskRunResponse { - id: number; - taskId: number; - sessionId: string; - status: string; - reportId: number | null; - outputSummary: string | null; - errorMessage: string | null; - startedAt: string; - finishedAt: string | null; -} - export function listScheduledTasks() { return request.get>('/scheduled-agent-tasks'); } @@ -79,21 +59,10 @@ export function deleteScheduledTask(id: number) { return request.delete>(`/scheduled-agent-tasks/${id}`); } -export function enableScheduledTask(id: number) { - return request.post>(`/scheduled-agent-tasks/${id}/enable`); -} - -export function disableScheduledTask(id: number) { - return request.post>(`/scheduled-agent-tasks/${id}/disable`); +export function updateScheduledTaskEnabled(id: number, enabled: boolean) { + return request.put>(`/scheduled-agent-tasks/${id}/enabled/${enabled}`); } export function runScheduledTask(id: number) { return request.post>(`/scheduled-agent-tasks/${id}/run`); } - -export function listScheduledTaskRuns(id: number, limit = 20) { - return request.get>( - `/scheduled-agent-tasks/${id}/runs`, - { params: { limit } }, - ); -} diff --git a/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue index 8481fde..083ef83 100644 --- a/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue +++ b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue @@ -22,17 +22,13 @@ import { createScheduledTask, deleteScheduledTask, - disableScheduledTask, - enableScheduledTask, - listScheduledTaskRuns, listScheduledTasks, runScheduledTask, + updateScheduledTaskEnabled, updateScheduledTask, type ScheduleType, type ScheduledTaskRequest, type ScheduledTaskResponse, - type ScheduledTaskRunResponse, - type SessionMode, } from '@/api/scheduledTask'; interface TaskForm { @@ -42,18 +38,14 @@ scheduleExpr: string; timezone: string; enabled: boolean; - sessionMode: SessionMode; - sessionId: string; } const rows = ref([]); - const runs = ref([]); const keyword = ref(''); const loading = ref(false); const runningTaskId = ref(null); const submitLoading = ref(false); const dialogVisible = ref(false); - const runsDrawerVisible = ref(false); const selectedTask = ref(null); const formRef = ref(); @@ -64,8 +56,6 @@ scheduleExpr: '09:00', timezone: 'Asia/Shanghai', enabled: true, - sessionMode: 'NEW_EACH_RUN', - sessionId: '', }); const rules: FormRules = { @@ -74,18 +64,6 @@ scheduleType: [{ required: true, message: '请选择调度类型', trigger: 'change' }], scheduleExpr: [{ required: true, message: '调度表达式不能为空', trigger: 'blur' }], timezone: [{ required: true, message: '时区不能为空', trigger: 'blur' }], - sessionId: [ - { - validator: (_rule, value, callback) => { - if (form.sessionMode === 'FIXED_SESSION' && !String(value || '').trim()) { - callback(new Error('固定会话模式需要填写 Session ID')); - return; - } - callback(); - }, - trigger: 'blur', - }, - ], }; const filteredRows = computed(() => { @@ -95,9 +73,7 @@ } return rows.value.filter( item => - item.name.toLowerCase().includes(value) || - item.prompt.toLowerCase().includes(value) || - (item.sessionId || '').toLowerCase().includes(value), + item.name.toLowerCase().includes(value) || item.prompt.toLowerCase().includes(value), ); }); @@ -129,8 +105,6 @@ scheduleExpr: '09:00', timezone: 'Asia/Shanghai', enabled: true, - sessionMode: 'NEW_EACH_RUN', - sessionId: '', }); } @@ -149,8 +123,6 @@ scheduleExpr: row.scheduleExpr, timezone: row.timezone, enabled: row.enabled, - sessionMode: row.sessionMode, - sessionId: row.sessionId ?? '', }); dialogVisible.value = true; } @@ -171,8 +143,6 @@ scheduleExpr: form.scheduleExpr.trim(), timezone: form.timezone.trim(), enabled: form.enabled, - sessionMode: form.sessionMode, - sessionId: form.sessionMode === 'FIXED_SESSION' ? form.sessionId.trim() : undefined, }; submitLoading.value = true; @@ -193,10 +163,10 @@ async function toggleEnabled(row: ScheduledTaskResponse) { if (row.enabled) { - await disableScheduledTask(row.id); + await updateScheduledTaskEnabled(row.id, false); ElMessage.success('任务已停用'); } else { - await enableScheduledTask(row.id); + await updateScheduledTaskEnabled(row.id, true); ElMessage.success('任务已启用'); } await loadTasks(); @@ -232,13 +202,6 @@ } } - async function openRuns(row: ScheduledTaskResponse) { - selectedTask.value = row; - runsDrawerVisible.value = true; - const response = await listScheduledTaskRuns(row.id); - runs.value = response.data.data ?? []; - } - function formatTime(value: string | null) { return value ? value.replace('T', ' ') : '-'; } @@ -265,7 +228,7 @@ v-model="keyword" class="keyword-field" clearable - placeholder="搜索任务 / 提示词 / Session" + placeholder="搜索任务 / 提示词" /> 刷新 新建任务 @@ -284,7 +247,6 @@ {{ row.enabled ? '启用' : '停用' }} - 运行中 @@ -306,7 +268,6 @@ 运行 - 记录 删除 @@ -343,19 +304,6 @@ - - - 每次新会话 - 固定会话 - - - - - @@ -365,25 +313,6 @@ 保存 - - - - - - - - - - - - - -
暂无运行记录
-
@@ -434,10 +363,4 @@ .task-table { width: 100%; } - - .empty-tip { - padding: 32px 0; - text-align: center; - color: var(--app-text-muted); - } diff --git a/sql/data_source.sql b/sql/data_source.sql index 160ac0f..89720f9 100644 --- a/sql/data_source.sql +++ b/sql/data_source.sql @@ -114,35 +114,12 @@ CREATE TABLE IF NOT EXISTS `scheduled_agent_task` ( `schedule_expr` VARCHAR(128) NOT NULL COMMENT 'HH:mm[:ss], ISO-8601 duration, or cron', `timezone` VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai' COMMENT 'Task timezone', `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether dispatch can pick up the task', - `running` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Whether a run currently owns the task', `lock_until` DATETIME DEFAULT NULL COMMENT 'Run lock expiration', `lock_owner` VARCHAR(64) DEFAULT NULL COMMENT 'Run lock owner token', - `current_run_id` INT DEFAULT NULL COMMENT 'Current run id while running', - `session_mode` VARCHAR(32) NOT NULL DEFAULT 'NEW_EACH_RUN' COMMENT 'NEW_EACH_RUN or FIXED_SESSION', - `session_id` VARCHAR(255) DEFAULT NULL COMMENT 'Fixed session id when session_mode is FIXED_SESSION', `next_run_at` DATETIME NOT NULL COMMENT 'Next due time', - `last_run_at` DATETIME DEFAULT NULL COMMENT 'Last dispatch time', `last_status` VARCHAR(32) DEFAULT NULL COMMENT 'Last run status', - `last_error` TEXT DEFAULT NULL COMMENT 'Last run error', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time', PRIMARY KEY (`id`), - KEY `idx_due_task` (`enabled`, `next_run_at`), - KEY `idx_lock` (`running`, `lock_until`), - KEY `idx_lock_owner` (`lock_owner`) + KEY `idx_due_task` (`enabled`, `next_run_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task'; - -CREATE TABLE IF NOT EXISTS `scheduled_agent_task_run` ( - `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Primary key', - `task_id` INT NOT NULL COMMENT 'Task id', - `session_id` VARCHAR(255) NOT NULL COMMENT 'Agent session id', - `status` VARCHAR(32) NOT NULL COMMENT 'RUNNING, SUCCESS, FAILED, or NEEDS_USER', - `report_id` INT DEFAULT NULL COMMENT 'Generated report id', - `output_summary` TEXT DEFAULT NULL COMMENT 'Final agent text or summary', - `error_message` TEXT DEFAULT NULL COMMENT 'Failure reason', - `started_at` DATETIME NOT NULL COMMENT 'Start time', - `finished_at` DATETIME DEFAULT NULL COMMENT 'Finish time', - PRIMARY KEY (`id`), - KEY `idx_task_started_at` (`task_id`, `started_at`), - KEY `idx_session_id` (`session_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Scheduled agent task run'; From 91439d091b36d5374f56afed0f5aac377a400b04 Mon Sep 17 00:00:00 2001 From: mengnankkkk Date: Fri, 7 Aug 2026 22:16:40 +0800 Subject: [PATCH 06/19] fix: remove --- .../ScheduledAgentTaskController.java | 83 +++++++--- .../dto/ScheduledAgentTaskRequest.java | 1 - .../dto/ScheduledAgentTaskResponse.java | 7 +- .../malonetalk/entity/ScheduledAgentTask.java | 4 - .../enums/ScheduledAgentTaskStatus.java | 24 --- .../mapper/ScheduledAgentTaskMapper.java | 11 +- ...asePollingScheduledAgentTaskScheduler.java | 90 ++++++++--- .../ScheduledAgentScheduleCalculator.java | 19 +-- .../service/ScheduledAgentTaskRunner.java | 141 ---------------- .../ScheduledAgentTaskServiceImpl.java | 150 ------------------ .../src/main/resources/application.properties | 6 - .../mapper/ScheduledAgentTaskMapper.xml | 42 ++--- data-agent-frontend/src/api/scheduledTask.ts | 13 +- .../scheduled-task/ScheduledTaskManage.vue | 28 ++-- sql/data_source.sql | 2 - 15 files changed, 154 insertions(+), 467 deletions(-) delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java delete mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java index e7b193e..1744331 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/ScheduledAgentTaskController.java @@ -17,12 +17,19 @@ */ package io.github.malonetalk.controller; +import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.common.Result; import io.github.malonetalk.dto.ScheduledAgentTaskRequest; import io.github.malonetalk.dto.ScheduledAgentTaskResponse; -import io.github.malonetalk.service.ScheduledAgentTaskServiceImpl; +import io.github.malonetalk.entity.ScheduledAgentTask; +import io.github.malonetalk.enums.ScheduledAgentScheduleType; +import io.github.malonetalk.exception.BusinessException; +import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; +import io.github.malonetalk.service.DatabasePollingScheduledAgentTaskScheduler; +import io.github.malonetalk.service.ScheduledAgentScheduleCalculator; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; @@ -41,50 +48,78 @@ @RequestMapping("/api/scheduled-agent-tasks") public class ScheduledAgentTaskController { - private final ScheduledAgentTaskServiceImpl taskService; + private final ScheduledAgentTaskMapper taskMapper; + private final ScheduledAgentScheduleCalculator scheduleCalculator; + private final DatabasePollingScheduledAgentTaskScheduler taskScheduler; @PostMapping - public Result create( - @Valid @RequestBody ScheduledAgentTaskRequest request) { - return Result.success(taskService.create(request)); + public Result create(@Valid @RequestBody ScheduledAgentTaskRequest request) { + ScheduledAgentTask task = buildTask(new ScheduledAgentTask(), request); + task.setNextRunAt( + scheduleCalculator.nextRunAfter( + task.getScheduleType(), task.getScheduleExpr(), LocalDateTime.now())); + taskMapper.insert(task); + return Result.success(true); } @PutMapping("/{id}") - public Result update( + public Result update( @PathVariable @Positive(message = "id must be positive.") Integer id, @Valid @RequestBody ScheduledAgentTaskRequest request) { - return Result.success(taskService.update(id, request)); + ScheduledAgentTask task = buildTask(new ScheduledAgentTask(), request); + task.setId(id); + task.setNextRunAt( + scheduleCalculator.nextRunAfter( + task.getScheduleType(), task.getScheduleExpr(), LocalDateTime.now())); + if (taskMapper.update(task) == 0) { + throw notFound(id); + } + return Result.success(true); } @DeleteMapping("/{id}") public Result delete( @PathVariable @Positive(message = "id must be positive.") Integer id) { - taskService.delete(id); + if (taskMapper.deleteById(id) == 0) { + throw notFound(id); + } return Result.success(true); } - @GetMapping("/{id}") - public Result getById( - @PathVariable @Positive(message = "id must be positive.") Integer id) { - return Result.success(taskService.getById(id)); - } - @GetMapping public Result> listAll() { - return Result.success(taskService.listAll()); - } - - @PutMapping("/{id}/enabled/{enabled}") - public Result updateEnabled( - @PathVariable @Positive(message = "id must be positive.") Integer id, - @PathVariable boolean enabled) { - taskService.updateEnabled(id, enabled); - return Result.success(true); + return Result.success(taskMapper.selectAll().stream().map(this::toResponse).toList()); } @PostMapping("/{id}/run") public Result runNow( @PathVariable @Positive(message = "id must be positive.") Integer id) { - return Result.success(taskService.runNow(id)); + return Result.success(taskScheduler.runNow(id)); + } + + private ScheduledAgentTask buildTask( + ScheduledAgentTask task, ScheduledAgentTaskRequest request) { + task.setName(request.name().trim()); + task.setPrompt(request.prompt().trim()); + task.setScheduleType(request.scheduleType().name()); + task.setScheduleExpr(request.scheduleExpr().trim()); + task.setEnabled(request.enabled() == null || request.enabled()); + return task; + } + + private BusinessException notFound(Integer id) { + return BusinessException.of( + ErrorCode.RESOURCE_NOT_FOUND, "Scheduled task does not exist: id=" + id); + } + + private ScheduledAgentTaskResponse toResponse(ScheduledAgentTask task) { + return new ScheduledAgentTaskResponse( + task.getId(), + task.getName(), + task.getPrompt(), + ScheduledAgentScheduleType.valueOf(task.getScheduleType()), + task.getScheduleExpr(), + task.getEnabled(), + task.getNextRunAt()); } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java index e61dd89..76ce4d1 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskRequest.java @@ -26,5 +26,4 @@ public record ScheduledAgentTaskRequest( @NotBlank(message = "prompt cannot be blank.") String prompt, @NotNull(message = "scheduleType cannot be null.") ScheduledAgentScheduleType scheduleType, @NotBlank(message = "scheduleExpr cannot be blank.") String scheduleExpr, - String timezone, Boolean enabled) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java index 72674f8..03bd906 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ScheduledAgentTaskResponse.java @@ -18,7 +18,6 @@ package io.github.malonetalk.dto; import io.github.malonetalk.enums.ScheduledAgentScheduleType; -import io.github.malonetalk.enums.ScheduledAgentTaskStatus; import java.time.LocalDateTime; public record ScheduledAgentTaskResponse( @@ -27,9 +26,5 @@ public record ScheduledAgentTaskResponse( String prompt, ScheduledAgentScheduleType scheduleType, String scheduleExpr, - String timezone, Boolean enabled, - LocalDateTime nextRunAt, - ScheduledAgentTaskStatus lastStatus, - LocalDateTime createTime, - LocalDateTime updateTime) {} + LocalDateTime nextRunAt) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java index 407a830..ceb82b7 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/ScheduledAgentTask.java @@ -28,10 +28,6 @@ public class ScheduledAgentTask { private String prompt; private String scheduleType; private String scheduleExpr; - private String timezone; private Boolean enabled; private LocalDateTime nextRunAt; - private String lastStatus; - private LocalDateTime createTime; - private LocalDateTime updateTime; } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java b/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java deleted file mode 100644 index 50ff72e..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/enums/ScheduledAgentTaskStatus.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.enums; - -public enum ScheduledAgentTaskStatus { - SUCCESS, - FAILED, - NEEDS_USER -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java index ac5c9c4..571c22f 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/ScheduledAgentTaskMapper.java @@ -30,19 +30,13 @@ public interface ScheduledAgentTaskMapper { int update(ScheduledAgentTask task); - int updateEnabled( - @Param("id") Integer id, - @Param("enabled") boolean enabled, - @Param("updateTime") LocalDateTime updateTime); - int deleteById(@Param("id") Integer id); ScheduledAgentTask selectById(@Param("id") Integer id); List selectAll(); - List findDueTasks( - @Param("now") LocalDateTime now, @Param("limit") int limit); + List findDueTaskIds(@Param("now") LocalDateTime now, @Param("limit") int limit); int lockForRun( @Param("id") Integer id, @@ -55,6 +49,5 @@ int finishRun( @Param("id") Integer id, @Param("lockOwner") String lockOwner, @Param("nextRunAt") LocalDateTime nextRunAt, - @Param("finishedAt") LocalDateTime finishedAt, - @Param("lastStatus") String lastStatus); + @Param("finishedAt") LocalDateTime finishedAt); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java index 5b58e83..3545bd1 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatabasePollingScheduledAgentTaskScheduler.java @@ -17,14 +17,16 @@ */ package io.github.malonetalk.service; +import io.github.malonetalk.agent.AgentService; +import io.github.malonetalk.entity.ScheduledAgentTask; import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import java.time.Duration; import java.time.LocalDateTime; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.core.task.TaskRejectedException; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -36,34 +38,28 @@ public class DatabasePollingScheduledAgentTaskScheduler { private static final int BATCH_SIZE = 20; + private static final int POOL_SIZE = 3; + private static final int QUEUE_CAPACITY = 20; + private static final Duration LOCK_DURATION = Duration.ofMinutes(30); private final ScheduledAgentTaskMapper taskMapper; - private final ScheduledAgentTaskRunner taskRunner; + private final AgentService agentService; + private final ScheduledAgentScheduleCalculator scheduleCalculator; private final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - @Value("${data-agent.schedule.executor.core-size:3}") - private int corePoolSize; - - @Value("${data-agent.schedule.executor.max-size:3}") - private int maxPoolSize; - - @Value("${data-agent.schedule.executor.queue-capacity:20}") - private int queueCapacity; - @PostConstruct public void initExecutor() { - executor.setCorePoolSize(corePoolSize); - executor.setMaxPoolSize(maxPoolSize); - executor.setQueueCapacity(queueCapacity); + executor.setCorePoolSize(POOL_SIZE); + executor.setMaxPoolSize(POOL_SIZE); + executor.setQueueCapacity(QUEUE_CAPACITY); executor.setThreadNamePrefix("scheduled-agent-task-"); - executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); } @Scheduled(fixedDelayString = "${data-agent.schedule.dispatch-delay-ms:10000}") public void dispatchDueTasks() { - for (var task : taskMapper.findDueTasks(LocalDateTime.now(), BATCH_SIZE)) { - ScheduledAgentTaskRunner.ClaimedRun claimedRun = taskRunner.claim(task.getId(), false); + for (Integer taskId : taskMapper.findDueTaskIds(LocalDateTime.now(), BATCH_SIZE)) { + ClaimedRun claimedRun = claim(taskId, false); if (claimedRun != null && !execute(claimedRun)) { return; } @@ -71,26 +67,76 @@ public void dispatchDueTasks() { } public boolean runNow(Integer taskId) { - ScheduledAgentTaskRunner.ClaimedRun claimedRun = taskRunner.claim(taskId, true); + ClaimedRun claimedRun = claim(taskId, true); if (claimedRun == null) { return false; } return execute(claimedRun); } - private boolean execute(ScheduledAgentTaskRunner.ClaimedRun claimedRun) { + private boolean execute(ClaimedRun claimedRun) { try { - executor.execute(() -> taskRunner.run(claimedRun)); + executor.execute(() -> run(claimedRun)); return true; } catch (TaskRejectedException e) { - taskRunner.reject(claimedRun); + finish(claimedRun.task(), claimedRun.lockOwner(), claimedRun.force()); log.warn("Scheduled agent task executor rejected taskId={}", claimedRun.task().getId()); return false; } } + private ClaimedRun claim(Integer taskId, boolean force) { + LocalDateTime startedAt = LocalDateTime.now(); + String lockOwner = UUID.randomUUID().toString(); + if (taskMapper.lockForRun( + taskId, startedAt, startedAt.plus(LOCK_DURATION), lockOwner, force) + == 0) { + return null; + } + + ScheduledAgentTask task = taskMapper.selectById(taskId); + String sessionId = "scheduled-task-" + task.getId() + "-" + UUID.randomUUID(); + return new ClaimedRun(task, sessionId, lockOwner, force); + } + + private void run(ClaimedRun claimedRun) { + try { + agentService + .chatStream(claimedRun.sessionId(), buildPrompt(claimedRun.task()), null, false) + .then() + .block(); + } catch (Exception e) { + log.error("Scheduled agent task failed: taskId={}", claimedRun.task().getId(), e); + } finally { + finish(claimedRun.task(), claimedRun.lockOwner(), claimedRun.force()); + } + } + + private void finish(ScheduledAgentTask task, String lockOwner, boolean force) { + LocalDateTime finishedAt = LocalDateTime.now(); + LocalDateTime nextRunAt = + force && task.getNextRunAt().isAfter(finishedAt) + ? task.getNextRunAt() + : scheduleCalculator.nextRunAfter( + task.getScheduleType(), task.getScheduleExpr(), finishedAt); + int updated = taskMapper.finishRun(task.getId(), lockOwner, nextRunAt, finishedAt); + if (updated == 0) { + log.warn("Scheduled agent task lock changed before finish: taskId={}", task.getId()); + } + } + + private String buildPrompt(ScheduledAgentTask task) { + return task.getPrompt() + + "\n\n" + + "Scheduled task requirement: if information is insufficient or user confirmation" + + " is needed, state that the task cannot be completed; do not call ask_user."; + } + @PreDestroy public void shutdown() { executor.shutdown(); } + + private record ClaimedRun( + ScheduledAgentTask task, String sessionId, String lockOwner, boolean force) {} } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java index d3a2d71..5636622 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentScheduleCalculator.java @@ -32,11 +32,11 @@ @Component public class ScheduledAgentScheduleCalculator { - public LocalDateTime nextRunAfter( - String type, String expr, LocalDateTime after, String timezone) { - ZoneId taskZone = ZoneId.of(normalizeTimezone(timezone)); + private static final ZoneId TASK_ZONE = ZoneId.of("Asia/Shanghai"); + + public LocalDateTime nextRunAfter(String type, String expr, LocalDateTime after) { ZoneId storageZone = ZoneId.systemDefault(); - ZonedDateTime afterInTaskZone = after.atZone(storageZone).withZoneSameInstant(taskZone); + ZonedDateTime afterInTaskZone = after.atZone(storageZone).withZoneSameInstant(TASK_ZONE); ZonedDateTime nextInTaskZone = switch (ScheduledAgentScheduleType.valueOf(type)) { case DAILY -> nextDaily(expr, afterInTaskZone); @@ -46,17 +46,6 @@ public LocalDateTime nextRunAfter( return nextInTaskZone.withZoneSameInstant(storageZone).toLocalDateTime(); } - public String normalizeTimezone(String timezone) { - if (timezone == null || timezone.isBlank()) { - throw invalidSchedule("timezone cannot be blank."); - } - try { - return ZoneId.of(timezone.trim()).getId(); - } catch (DateTimeException e) { - throw invalidSchedule("Unsupported timezone: " + timezone); - } - } - private ZonedDateTime nextDaily(String expr, ZonedDateTime after) { LocalTime time = parseDailyTime(expr); ZonedDateTime next = after.toLocalDate().atTime(time).atZone(after.getZone()); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java deleted file mode 100644 index 7a6f621..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskRunner.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.service; - -import io.github.malonetalk.agent.AgentService; -import io.github.malonetalk.dto.ChatStreamEvent; -import io.github.malonetalk.entity.ScheduledAgentTask; -import io.github.malonetalk.enums.ChatStreamEventType; -import io.github.malonetalk.enums.ScheduledAgentTaskStatus; -import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ScheduledAgentTaskRunner { - - private final AgentService agentService; - private final ScheduledAgentTaskMapper taskMapper; - private final ScheduledAgentScheduleCalculator scheduleCalculator; - - @Value("${data-agent.schedule.lock-duration:PT30M}") - private Duration lockDuration = Duration.ofMinutes(30); - - public ClaimedRun claim(Integer taskId, boolean force) { - LocalDateTime startedAt = LocalDateTime.now(); - String lockOwner = UUID.randomUUID().toString(); - if (taskMapper.lockForRun( - taskId, - startedAt, - startedAt.plus(effectiveLockDuration()), - lockOwner, - force) - == 0) { - return null; - } - - ScheduledAgentTask task = taskMapper.selectById(taskId); - String sessionId = "scheduled-task-" + task.getId() + "-" + UUID.randomUUID(); - return new ClaimedRun(task, sessionId, lockOwner, force); - } - - public void run(ClaimedRun claimedRun) { - ScheduledAgentTaskStatus status = ScheduledAgentTaskStatus.SUCCESS; - try { - List events = - agentService - .chatStream( - claimedRun.sessionId(), - buildPrompt(claimedRun.task()), - null, - false) - .collectList() - .block(); - status = resolveStatus(events); - } catch (Exception e) { - status = ScheduledAgentTaskStatus.FAILED; - log.error("Scheduled agent task failed: taskId={}", claimedRun.task().getId(), e); - } finally { - finish(claimedRun.task(), claimedRun.lockOwner(), claimedRun.force(), status); - } - } - - public void reject(ClaimedRun claimedRun) { - finish( - claimedRun.task(), - claimedRun.lockOwner(), - claimedRun.force(), - ScheduledAgentTaskStatus.FAILED); - } - - private void finish( - ScheduledAgentTask task, - String lockOwner, - boolean force, - ScheduledAgentTaskStatus status) { - LocalDateTime finishedAt = LocalDateTime.now(); - LocalDateTime nextRunAt = - force && task.getNextRunAt().isAfter(finishedAt) - ? task.getNextRunAt() - : scheduleCalculator.nextRunAfter( - task.getScheduleType(), - task.getScheduleExpr(), - finishedAt, - task.getTimezone()); - int updated = - taskMapper.finishRun(task.getId(), lockOwner, nextRunAt, finishedAt, status.name()); - if (updated == 0) { - log.warn("Scheduled agent task lock changed before finish: taskId={}", task.getId()); - } - } - - private String buildPrompt(ScheduledAgentTask task) { - return task.getPrompt() + "\n\n定时任务执行要求:如果信息不足或需要用户确认,请直接说明无法完成,不要调用 ask_user 反问用户。"; - } - - private ScheduledAgentTaskStatus resolveStatus(List events) { - if (events == null) { - return ScheduledAgentTaskStatus.FAILED; - } - if (events.stream().anyMatch(event -> event.type() == ChatStreamEventType.ERROR)) { - return ScheduledAgentTaskStatus.FAILED; - } - if (events.stream().anyMatch(event -> event.type() == ChatStreamEventType.QUESTION)) { - return ScheduledAgentTaskStatus.NEEDS_USER; - } - return ScheduledAgentTaskStatus.SUCCESS; - } - - private Duration effectiveLockDuration() { - if (lockDuration.isZero() || lockDuration.isNegative()) { - throw new IllegalStateException("data-agent.schedule.lock-duration must be positive."); - } - return lockDuration; - } - - public record ClaimedRun( - ScheduledAgentTask task, String sessionId, String lockOwner, boolean force) {} -} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java deleted file mode 100644 index 69eac6e..0000000 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/ScheduledAgentTaskServiceImpl.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2026 github.com/MaloneTalk - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * limitations under the License. - */ -package io.github.malonetalk.service; - -import io.github.malonetalk.common.ErrorCode; -import io.github.malonetalk.dto.ScheduledAgentTaskRequest; -import io.github.malonetalk.dto.ScheduledAgentTaskResponse; -import io.github.malonetalk.entity.ScheduledAgentTask; -import io.github.malonetalk.enums.ScheduledAgentScheduleType; -import io.github.malonetalk.enums.ScheduledAgentTaskStatus; -import io.github.malonetalk.exception.BusinessException; -import io.github.malonetalk.mapper.ScheduledAgentTaskMapper; -import io.github.malonetalk.utils.RequestAssert; -import java.time.LocalDateTime; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ScheduledAgentTaskServiceImpl { - - private static final String DEFAULT_TIMEZONE = "Asia/Shanghai"; - - private final ScheduledAgentTaskMapper taskMapper; - private final ScheduledAgentScheduleCalculator scheduleCalculator; - private final DatabasePollingScheduledAgentTaskScheduler taskScheduler; - - public ScheduledAgentTaskResponse create(ScheduledAgentTaskRequest request) { - ScheduledAgentTask task = buildTask(new ScheduledAgentTask(), request); - LocalDateTime now = LocalDateTime.now(); - task.setNextRunAt( - scheduleCalculator.nextRunAfter( - task.getScheduleType(), task.getScheduleExpr(), now, task.getTimezone())); - task.setCreateTime(now); - task.setUpdateTime(now); - taskMapper.insert(task); - return toResponse(task); - } - - public ScheduledAgentTaskResponse update(Integer id, ScheduledAgentTaskRequest request) { - ScheduledAgentTask existing = getTask(id); - ScheduledAgentTask task = buildTask(existing, request); - task.setNextRunAt( - scheduleCalculator.nextRunAfter( - task.getScheduleType(), - task.getScheduleExpr(), - LocalDateTime.now(), - task.getTimezone())); - task.setUpdateTime(LocalDateTime.now()); - taskMapper.update(task); - ScheduledAgentTask saved = taskMapper.selectById(id); - return toResponse(saved); - } - - public void delete(Integer id) { - RequestAssert.requireNonNegative(id, "id must be non-negative."); - if (taskMapper.deleteById(id) == 0) { - throw notFound(id); - } - } - - public ScheduledAgentTaskResponse getById(Integer id) { - return toResponse(getTask(id)); - } - - public List listAll() { - return taskMapper.selectAll().stream().map(this::toResponse).toList(); - } - - public void updateEnabled(Integer id, boolean enabled) { - RequestAssert.requireNonNegative(id, "id must be non-negative."); - if (taskMapper.updateEnabled(id, enabled, LocalDateTime.now()) == 0) { - throw notFound(id); - } - } - - public boolean runNow(Integer id) { - return taskScheduler.runNow(id); - } - - private ScheduledAgentTask buildTask( - ScheduledAgentTask task, ScheduledAgentTaskRequest request) { - String timezone = - request.timezone() == null || request.timezone().isBlank() - ? DEFAULT_TIMEZONE - : request.timezone().trim(); - timezone = scheduleCalculator.normalizeTimezone(timezone); - - ScheduledAgentScheduleType scheduleType = request.scheduleType(); - - task.setName(RequestAssert.requireNotBlank(request.name(), "name cannot be blank.")); - task.setPrompt(RequestAssert.requireNotBlank(request.prompt(), "prompt cannot be blank.")); - task.setScheduleType(scheduleType.name()); - task.setScheduleExpr( - RequestAssert.requireNotBlank( - request.scheduleExpr(), "scheduleExpr cannot be blank.")); - task.setTimezone(timezone); - task.setEnabled(request.enabled() == null || request.enabled()); - return task; - } - - private ScheduledAgentTask getTask(Integer id) { - RequestAssert.requireNonNegative(id, "id must be non-negative."); - ScheduledAgentTask task = taskMapper.selectById(id); - if (task == null) { - throw notFound(id); - } - return task; - } - - private BusinessException notFound(Integer id) { - return BusinessException.of( - ErrorCode.RESOURCE_NOT_FOUND, "Scheduled task does not exist: id=" + id); - } - - private ScheduledAgentTaskResponse toResponse(ScheduledAgentTask task) { - return new ScheduledAgentTaskResponse( - task.getId(), - task.getName(), - task.getPrompt(), - ScheduledAgentScheduleType.valueOf(task.getScheduleType()), - task.getScheduleExpr(), - task.getTimezone(), - task.getEnabled(), - task.getNextRunAt(), - toStatus(task.getLastStatus()), - task.getCreateTime(), - task.getUpdateTime()); - } - - private ScheduledAgentTaskStatus toStatus(String status) { - return status == null ? null : ScheduledAgentTaskStatus.valueOf(status); - } -} diff --git a/data-agent-backend/src/main/resources/application.properties b/data-agent-backend/src/main/resources/application.properties index 8350f8c..02d19fb 100644 --- a/data-agent-backend/src/main/resources/application.properties +++ b/data-agent-backend/src/main/resources/application.properties @@ -20,12 +20,6 @@ mybatis.type-aliases-package=io.github.malonetalk.entity mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl -# Scheduled Agent Task Configuration -data-agent.schedule.lock-duration=${SCHEDULE_LOCK_DURATION:PT30M} -data-agent.schedule.executor.core-size=${SCHEDULE_EXECUTOR_CORE_SIZE:3} -data-agent.schedule.executor.max-size=${SCHEDULE_EXECUTOR_MAX_SIZE:3} -data-agent.schedule.executor.queue-capacity=${SCHEDULE_EXECUTOR_QUEUE_CAPACITY:20} - # Model Configuration io.github.malonetalk.model.provider=dashscope io.github.malonetalk.model.name=qwen3-max diff --git a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml index af7474e..e79b33f 100644 --- a/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/ScheduledAgentTaskMapper.xml @@ -2,30 +2,14 @@ - - - - - - - - - - - - - - INSERT INTO scheduled_agent_task ( - name, prompt, schedule_type, schedule_expr, timezone, - enabled, next_run_at, - create_time, update_time + name, prompt, schedule_type, schedule_expr, + enabled, next_run_at ) VALUES ( - #{name}, #{prompt}, #{scheduleType}, #{scheduleExpr}, #{timezone}, - #{enabled}, #{nextRunAt}, - #{createTime}, #{updateTime} + #{name}, #{prompt}, #{scheduleType}, #{scheduleExpr}, + #{enabled}, #{nextRunAt} ) @@ -35,16 +19,9 @@ prompt = #{prompt}, schedule_type = #{scheduleType}, schedule_expr = #{scheduleExpr}, - timezone = #{timezone}, enabled = #{enabled}, next_run_at = #{nextRunAt}, - update_time = #{updateTime} - WHERE id = #{id} - - - - UPDATE scheduled_agent_task - SET enabled = #{enabled}, update_time = #{updateTime} + update_time = NOW() WHERE id = #{id} @@ -53,19 +30,19 @@ WHERE id = #{id} - SELECT * FROM scheduled_agent_task WHERE id = #{id} LIMIT 1 - SELECT * FROM scheduled_agent_task ORDER BY id DESC - + SELECT id FROM scheduled_agent_task WHERE enabled = 1 AND next_run_at <= #{now} AND (lock_until IS NULL OR lock_until < #{now}) @@ -91,7 +68,6 @@ SET lock_until = NULL, lock_owner = NULL, next_run_at = #{nextRunAt}, - last_status = #{lastStatus}, update_time = #{finishedAt} WHERE id = #{id} AND lock_owner = #{lockOwner} diff --git a/data-agent-frontend/src/api/scheduledTask.ts b/data-agent-frontend/src/api/scheduledTask.ts index e6cf4a3..ead29d2 100644 --- a/data-agent-frontend/src/api/scheduledTask.ts +++ b/data-agent-frontend/src/api/scheduledTask.ts @@ -25,7 +25,6 @@ export interface ScheduledTaskRequest { prompt: string; scheduleType: ScheduleType; scheduleExpr: string; - timezone?: string; enabled?: boolean; } @@ -35,12 +34,8 @@ export interface ScheduledTaskResponse { prompt: string; scheduleType: ScheduleType; scheduleExpr: string; - timezone: string; enabled: boolean; nextRunAt: string; - lastStatus: string | null; - createTime: string; - updateTime: string; } export function listScheduledTasks() { @@ -48,21 +43,17 @@ export function listScheduledTasks() { } export function createScheduledTask(data: ScheduledTaskRequest) { - return request.post>('/scheduled-agent-tasks', data); + return request.post>('/scheduled-agent-tasks', data); } export function updateScheduledTask(id: number, data: ScheduledTaskRequest) { - return request.put>(`/scheduled-agent-tasks/${id}`, data); + return request.put>(`/scheduled-agent-tasks/${id}`, data); } export function deleteScheduledTask(id: number) { return request.delete>(`/scheduled-agent-tasks/${id}`); } -export function updateScheduledTaskEnabled(id: number, enabled: boolean) { - return request.put>(`/scheduled-agent-tasks/${id}/enabled/${enabled}`); -} - export function runScheduledTask(id: number) { return request.post>(`/scheduled-agent-tasks/${id}/run`); } diff --git a/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue index 083ef83..81ac837 100644 --- a/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue +++ b/data-agent-frontend/src/views/scheduled-task/ScheduledTaskManage.vue @@ -24,7 +24,6 @@ deleteScheduledTask, listScheduledTasks, runScheduledTask, - updateScheduledTaskEnabled, updateScheduledTask, type ScheduleType, type ScheduledTaskRequest, @@ -36,7 +35,6 @@ prompt: string; scheduleType: ScheduleType; scheduleExpr: string; - timezone: string; enabled: boolean; } @@ -54,7 +52,6 @@ prompt: '', scheduleType: 'DAILY', scheduleExpr: '09:00', - timezone: 'Asia/Shanghai', enabled: true, }); @@ -63,7 +60,6 @@ prompt: [{ required: true, message: '提示词不能为空', trigger: 'blur' }], scheduleType: [{ required: true, message: '请选择调度类型', trigger: 'change' }], scheduleExpr: [{ required: true, message: '调度表达式不能为空', trigger: 'blur' }], - timezone: [{ required: true, message: '时区不能为空', trigger: 'blur' }], }; const filteredRows = computed(() => { @@ -103,7 +99,6 @@ prompt: '', scheduleType: 'DAILY', scheduleExpr: '09:00', - timezone: 'Asia/Shanghai', enabled: true, }); } @@ -121,7 +116,6 @@ prompt: row.prompt, scheduleType: row.scheduleType, scheduleExpr: row.scheduleExpr, - timezone: row.timezone, enabled: row.enabled, }); dialogVisible.value = true; @@ -141,7 +135,6 @@ prompt: form.prompt.trim(), scheduleType: form.scheduleType, scheduleExpr: form.scheduleExpr.trim(), - timezone: form.timezone.trim(), enabled: form.enabled, }; @@ -162,11 +155,18 @@ } async function toggleEnabled(row: ScheduledTaskResponse) { + const payload: ScheduledTaskRequest = { + name: row.name, + prompt: row.prompt, + scheduleType: row.scheduleType, + scheduleExpr: row.scheduleExpr, + enabled: !row.enabled, + }; if (row.enabled) { - await updateScheduledTaskEnabled(row.id, false); + await updateScheduledTask(row.id, payload); ElMessage.success('任务已停用'); } else { - await updateScheduledTaskEnabled(row.id, true); + await updateScheduledTask(row.id, payload); ElMessage.success('任务已启用'); } await loadTasks(); @@ -240,7 +240,6 @@ -