mateclaw/mateclaw-server/src/main/java/vip/mate/tool/builtin/CronJobTool.java

170 lines
7.7 KiB
Java

package vip.mate.tool.builtin;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import vip.mate.cron.model.CronJobDTO;
import vip.mate.cron.service.CronJobService;
import java.util.List;
/**
* Built-in tool: scheduled task (cron job) management via chat.
* <p>
* Allows agents to create, list, toggle, and delete cron jobs through natural language.
* The agent_id is automatically bound to the current agent. LLM generates cron expressions
* from natural language (e.g. "every day at 9am" → "0 9 * * *").
*
* @author MateClaw Team
* @see vip.mate.cron.service.CronJobService
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CronJobTool {
private final CronJobService cronJobService;
@vip.mate.tool.ConcurrencyUnsafe("cron job creation persists to mate_cron_job; concurrent creates can race on name")
@Tool(description = "Create a scheduled task (cron job). The task will run automatically at the specified time "
+ "and send the trigger message to the current agent. Use 5-field cron expressions: minute hour day month weekday. "
+ "Examples: '0 9 * * *' = daily at 9am, '0 9 * * 1-5' = weekdays at 9am, '*/30 * * * *' = every 30 minutes.")
public String create_cron_job(
@ToolParam(description = "Task name, e.g. 'Daily AI News Summary'") String name,
@ToolParam(description = "5-field cron expression: minute hour day month weekday") String cronExpression,
@ToolParam(description = "Message to send when the task triggers, e.g. 'Search for the latest AI news and summarize'") String triggerMessage,
@ToolParam(description = "Timezone, default Asia/Shanghai. Examples: UTC, America/New_York", required = false) String timezone) {
try {
// Resolve current agent ID from conversation context
String conversationId = ToolExecutionContext.conversationId();
Long agentId = resolveAgentId(conversationId);
CronJobDTO dto = new CronJobDTO();
dto.setName(name);
dto.setCronExpression(cronExpression);
dto.setTriggerMessage(triggerMessage);
dto.setTimezone(timezone != null && !timezone.isBlank() ? timezone : "Asia/Shanghai");
dto.setAgentId(agentId);
dto.setTaskType("text");
dto.setEnabled(true);
CronJobDTO created = cronJobService.create(dto);
JSONObject result = new JSONObject();
result.set("success", true);
result.set("jobId", created.getId());
result.set("name", created.getName());
result.set("cronExpression", created.getCronExpression());
result.set("timezone", created.getTimezone());
result.set("nextRunTime", created.getNextRunTime() != null ? created.getNextRunTime().toString() : "");
result.set("enabled", created.getEnabled());
return JSONUtil.toJsonPrettyStr(result);
} catch (Exception e) {
log.error("[CronJobTool] create failed: {}", e.getMessage());
return errorResult("Failed to create cron job: " + e.getMessage());
}
}
@Tool(description = "List all scheduled tasks (cron jobs) for the current agent. "
+ "Returns task name, cron expression, next run time, enabled status, and last run time.")
public String list_cron_jobs() {
try {
List<CronJobDTO> jobs = cronJobService.list();
JSONArray arr = new JSONArray();
for (CronJobDTO job : jobs) {
JSONObject obj = new JSONObject();
obj.set("jobId", job.getId());
obj.set("name", job.getName());
obj.set("cronExpression", job.getCronExpression());
obj.set("timezone", job.getTimezone());
obj.set("enabled", job.getEnabled());
obj.set("nextRunTime", job.getNextRunTime() != null ? job.getNextRunTime().toString() : "");
obj.set("lastRunTime", job.getLastRunTime() != null ? job.getLastRunTime().toString() : "");
obj.set("agentName", job.getAgentName());
arr.add(obj);
}
JSONObject result = new JSONObject();
result.set("totalJobs", jobs.size());
result.set("jobs", arr);
return JSONUtil.toJsonPrettyStr(result);
} catch (Exception e) {
log.error("[CronJobTool] list failed: {}", e.getMessage());
return errorResult("Failed to list cron jobs: " + e.getMessage());
}
}
@vip.mate.tool.ConcurrencyUnsafe("toggles row state in mate_cron_job; serialize to keep enabled/disabled deterministic")
@Tool(description = "Enable or disable a scheduled task by its job ID. "
+ "Use list_cron_jobs first to find the job ID.")
public String toggle_cron_job(
@ToolParam(description = "Job ID (number)") Long jobId,
@ToolParam(description = "true to enable, false to disable") Boolean enabled) {
try {
cronJobService.toggle(jobId, enabled);
CronJobDTO updated = cronJobService.getById(jobId);
JSONObject result = new JSONObject();
result.set("success", true);
result.set("jobId", jobId);
result.set("name", updated.getName());
result.set("enabled", updated.getEnabled());
result.set("nextRunTime", updated.getNextRunTime() != null ? updated.getNextRunTime().toString() : "");
return JSONUtil.toJsonPrettyStr(result);
} catch (Exception e) {
log.error("[CronJobTool] toggle failed: {}", e.getMessage());
return errorResult("Failed to toggle cron job: " + e.getMessage());
}
}
@vip.mate.tool.ConcurrencyUnsafe("destructive — removes row from mate_cron_job")
@Tool(description = "Delete a scheduled task by its job ID. This action requires user approval. "
+ "Use list_cron_jobs first to find the job ID.")
public String delete_cron_job(
@ToolParam(description = "Job ID (number) to delete") Long jobId) {
try {
CronJobDTO job = cronJobService.getById(jobId);
String jobName = job.getName();
cronJobService.delete(jobId);
JSONObject result = new JSONObject();
result.set("success", true);
result.set("deleted", jobName);
return JSONUtil.toJsonPrettyStr(result);
} catch (Exception e) {
log.error("[CronJobTool] delete failed: {}", e.getMessage());
return errorResult("Failed to delete cron job: " + e.getMessage());
}
}
/**
* Resolve agent ID from conversation ID.
* Convention: cron conversations use "cron:{jobId}", normal chats use "{agentId}:{uuid}".
*/
private Long resolveAgentId(String conversationId) {
if (conversationId == null || conversationId.isBlank()) {
return 1L; // default agent
}
// Try to extract agent ID from conversation metadata
// For now, use default agent ID 1 (the conversation's agent binding is handled by the caller)
try {
// Convention: conversationId might contain agent context info
// Fallback to first enabled agent
return 1L;
} catch (Exception e) {
return 1L;
}
}
private String errorResult(String message) {
JSONObject result = new JSONObject();
result.set("success", false);
result.set("error", message);
return JSONUtil.toJsonPrettyStr(result);
}
}