diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/CronJobTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/CronJobTool.java new file mode 100644 index 00000000..ca6c1b7c --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/CronJobTool.java @@ -0,0 +1,166 @@ +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. + *

+ * 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; + + @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 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()); + } + } + + @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()); + } + } + + @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); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java b/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java index cfc462d1..3ace7861 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/guard/DefaultToolGuard.java @@ -36,6 +36,12 @@ public class DefaultToolGuard implements ToolGuard { "edit_file" ); + /** 定时任务变更工具 —— 创建和删除需要用户审批 */ + private static final Set CRON_APPROVAL_TOOL_NAMES = Set.of( + "create_cron_job", + "delete_cron_job" + ); + /** 极端破坏性模式 —— 即使是 shell 工具也直接 BLOCK,不允许审批覆盖 */ private final List absoluteBlockPatterns; @@ -94,6 +100,12 @@ public class DefaultToolGuard implements ToolGuard { return ToolGuardResult.needsApproval("File write/edit operation requires user approval", "file_write_tool_default"); } + // 定时任务创建/删除需要审批 + if (toolName != null && CRON_APPROVAL_TOOL_NAMES.contains(toolName)) { + log.info("[ToolGuard] NEEDS_APPROVAL (cron job tool): tool={}", toolName); + return ToolGuardResult.needsApproval("Cron job create/delete requires user approval", "cron_tool_default"); + } + return ToolGuardResult.allow(); } diff --git a/mateclaw-server/src/main/resources/db/data-en.sql b/mateclaw-server/src/main/resources/db/data-en.sql index 7e62a9db..a3b975a9 100644 --- a/mateclaw-server/src/main/resources/db/data-en.sql +++ b/mateclaw-server/src/main/resources/db/data-en.sql @@ -385,6 +385,11 @@ MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, KEY (id) VALUES (1000000017, 'WikiTool', 'Wiki Knowledge Base', 'Read, search, and trace sources in Wiki knowledge bases. Supports wiki_read_page, wiki_list_pages, wiki_search_pages, wiki_trace_source.', 'builtin', 'wikiTool', '📚', TRUE, TRUE, NOW(), NOW(), 0); +-- Built-in tool: Cron Job Management (chat-based scheduled task management) +MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +KEY (id) +VALUES (1000000018, 'CronJobTool', 'Scheduled Tasks', 'Create, list, enable/disable, and delete scheduled tasks (cron jobs) through chat. Supports 5-field cron expressions for flexible scheduling.', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0); + -- Example MCP Server: Filesystem (see MateClaw docs mcpServers.filesystem) MERGE INTO mate_mcp_server ( id, name, description, transport, url, headers_json, command, args_json, env_json, cwd, diff --git a/mateclaw-server/src/main/resources/db/data-mysql-en.sql b/mateclaw-server/src/main/resources/db/data-mysql-en.sql index 2f64af3b..72690c74 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-en.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-en.sql @@ -386,6 +386,11 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name VALUES (1000000017, 'WikiTool', 'Wiki Knowledge Base', 'Read, search, and trace sources in Wiki knowledge bases. Supports wiki_read_page, wiki_list_pages, wiki_search_pages, wiki_trace_source.', 'builtin', 'wikiTool', '📚', TRUE, TRUE, NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted); +-- Built-in tool: Cron Job Management +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000018, 'CronJobTool', 'Scheduled Tasks', 'Create, list, enable/disable, and delete scheduled tasks (cron jobs) through chat. Supports 5-field cron expressions.', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0) +ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted); + -- Example MCP Server: Filesystem (see MateClaw docs mcpServers.filesystem) INSERT INTO mate_mcp_server (id, name, description, transport, url, headers_json, command, args_json, env_json, cwd, enabled, connect_timeout_seconds, read_timeout_seconds, last_status, last_error, diff --git a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql index 29cfcb6c..b484b086 100644 --- a/mateclaw-server/src/main/resources/db/data-mysql-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-mysql-zh.sql @@ -386,6 +386,11 @@ INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name VALUES (1000000017, 'WikiTool', 'Wiki 知识库', '读取、搜索 Wiki 知识库中的结构化页面,并追溯原始来源文件。支持 wiki_read_page、wiki_list_pages、wiki_search_pages、wiki_trace_source 四个工具。', 'builtin', 'wikiTool', '📚', TRUE, TRUE, NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted); +-- 内置工具:定时任务管理 +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000018, 'CronJobTool', '定时任务', '通过对话创建、查看、启停和删除定时任务。支持 5 字段 cron 表达式,灵活设定执行时间。', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0) +ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), tool_type=VALUES(tool_type), bean_name=VALUES(bean_name), icon=VALUES(icon), enabled=VALUES(enabled), builtin=VALUES(builtin), update_time=VALUES(update_time), deleted=VALUES(deleted); + -- 示例 MCP Server:Filesystem(参考 MateClaw 文档中的 mcpServers.filesystem) INSERT INTO mate_mcp_server ( id, name, description, transport, url, headers_json, command, args_json, env_json, cwd, diff --git a/mateclaw-server/src/main/resources/db/data-zh.sql b/mateclaw-server/src/main/resources/db/data-zh.sql index ca2c93f2..1085370b 100644 --- a/mateclaw-server/src/main/resources/db/data-zh.sql +++ b/mateclaw-server/src/main/resources/db/data-zh.sql @@ -391,6 +391,11 @@ MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, KEY (id) VALUES (1000000017, 'WikiTool', 'Wiki 知识库', '读取、搜索 Wiki 知识库中的结构化页面,并追溯原始来源文件。支持 wiki_read_page、wiki_list_pages、wiki_search_pages、wiki_trace_source 四个工具。', 'builtin', 'wikiTool', '📚', TRUE, TRUE, NOW(), NOW(), 0); +-- 内置工具:定时任务管理 +MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +KEY (id) +VALUES (1000000018, 'CronJobTool', '定时任务', '通过对话创建、查看、启停和删除定时任务。支持 5 字段 cron 表达式,灵活设定执行时间。', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0); + -- 示例 MCP Server:Filesystem(参考 MateClaw 文档中的 mcpServers.filesystem) MERGE INTO mate_mcp_server ( id, name, description, transport, url, headers_json, command, args_json, env_json, cwd, diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V2__workspace_base_path.sql b/mateclaw-server/src/main/resources/db/migration/h2/V2__workspace_base_path.sql new file mode 100644 index 00000000..127d064c --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V2__workspace_base_path.sql @@ -0,0 +1,2 @@ +-- V2: Add workspace base_path for directory restriction (RFC-002) +ALTER TABLE mate_workspace ADD COLUMN IF NOT EXISTS base_path VARCHAR(512); diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V3__register_cron_job_tool.sql b/mateclaw-server/src/main/resources/db/migration/h2/V3__register_cron_job_tool.sql new file mode 100644 index 00000000..e9b9bf3f --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V3__register_cron_job_tool.sql @@ -0,0 +1,4 @@ +-- V3: Register CronJobTool as built-in tool (RFC-003) +MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +KEY (id) +VALUES (1000000018, 'CronJobTool', 'Scheduled Tasks', 'Create, list, enable/disable, and delete scheduled tasks (cron jobs) through chat.', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V2__workspace_base_path.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V2__workspace_base_path.sql new file mode 100644 index 00000000..127d064c --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V2__workspace_base_path.sql @@ -0,0 +1,2 @@ +-- V2: Add workspace base_path for directory restriction (RFC-002) +ALTER TABLE mate_workspace ADD COLUMN IF NOT EXISTS base_path VARCHAR(512); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V3__register_cron_job_tool.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V3__register_cron_job_tool.sql new file mode 100644 index 00000000..d1e07372 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V3__register_cron_job_tool.sql @@ -0,0 +1,4 @@ +-- V3: Register CronJobTool as built-in tool (RFC-003) +INSERT INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +VALUES (1000000018, 'CronJobTool', 'Scheduled Tasks', 'Create, list, enable/disable, and delete scheduled tasks (cron jobs) through chat.', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0) +ON DUPLICATE KEY UPDATE name=VALUES(name), display_name=VALUES(display_name), description=VALUES(description), update_time=VALUES(update_time); diff --git a/mateclaw-server/src/main/resources/db/tools-sync.sql b/mateclaw-server/src/main/resources/db/tools-sync.sql index 33a8584c..ea96a26c 100644 --- a/mateclaw-server/src/main/resources/db/tools-sync.sql +++ b/mateclaw-server/src/main/resources/db/tools-sync.sql @@ -64,3 +64,7 @@ VALUES (1000000016, 'SqlQueryTool', 'SQL 查询', '在外部数据源上执行 MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) KEY (id) VALUES (1000000017, 'WikiTool', 'Wiki 知识库', '读取、搜索 Wiki 知识库中的结构化页面,并追溯原始来源文件。支持 wiki_read_page、wiki_list_pages、wiki_search_pages、wiki_trace_source 四个工具。', 'builtin', 'wikiTool', '📚', TRUE, TRUE, NOW(), NOW(), 0); + +MERGE INTO mate_tool (id, name, display_name, description, tool_type, bean_name, icon, enabled, builtin, create_time, update_time, deleted) +KEY (id) +VALUES (1000000018, 'CronJobTool', '定时任务', '通过对话创建、查看、启停和删除定时任务。支持 5 字段 cron 表达式,灵活设定执行时间。', 'builtin', 'cronJobTool', '⏰', TRUE, TRUE, NOW(), NOW(), 0); diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties index 2ce9a50b..e0237a8e 100644 --- a/mateclaw-server/src/main/resources/messages.properties +++ b/mateclaw-server/src/main/resources/messages.properties @@ -43,6 +43,10 @@ tool.search.param.freshness=\u65f6\u95f4\u8303\u56f4\u8fc7\u6ee4: day (\u4eca\u5 tool.search.param.language=\u8bed\u8a00\u504f\u597d: zh-CN (\u4e2d\u6587), en (\u82f1\u6587) tool.search.param.count=\u6700\u5927\u7ed3\u679c\u6570\u91cf: 1-10, \u9ed8\u8ba4 5 +tool.create_cron_job.desc=\u521b\u5efa\u5b9a\u65f6\u4efb\u52a1\u3002\u4efb\u52a1\u5c06\u5728\u6307\u5b9a\u65f6\u95f4\u81ea\u52a8\u8fd0\u884c\u5e76\u5411\u5f53\u524d Agent \u53d1\u9001\u89e6\u53d1\u6d88\u606f\u3002\u4f7f\u7528 5 \u5b57\u6bb5 cron \u8868\u8fbe\u5f0f\uff1a\u5206 \u65f6 \u65e5 \u6708 \u5468\u3002 +tool.list_cron_jobs.desc=\u5217\u51fa\u6240\u6709\u5b9a\u65f6\u4efb\u52a1\uff0c\u5305\u542b\u540d\u79f0\u3001cron \u8868\u8fbe\u5f0f\u3001\u4e0b\u6b21\u8fd0\u884c\u65f6\u95f4\u3001\u542f\u7528\u72b6\u6001\u3002 +tool.toggle_cron_job.desc=\u542f\u7528\u6216\u7981\u7528\u6307\u5b9a\u7684\u5b9a\u65f6\u4efb\u52a1\u3002 +tool.delete_cron_job.desc=\u5220\u9664\u6307\u5b9a\u7684\u5b9a\u65f6\u4efb\u52a1\uff0c\u9700\u8981\u7528\u6237\u5ba1\u6279\u3002 tool.delegateToAgent.desc=\u59d4\u6d3e\u4efb\u52a1\u7ed9\u53e6\u4e00\u4e2a Agent \u6267\u884c\uff0c\u5b9e\u73b0\u591a Agent \u534f\u4f5c\u3002\u76ee\u6807 Agent \u5c06\u5728\u72ec\u7acb\u4f1a\u8bdd\u4e2d\u6267\u884c\u4efb\u52a1\uff0c\u8fd4\u56de\u5176\u6700\u7ec8\u56de\u590d\u3002\u9700\u63d0\u4f9b\u5b8c\u6574\u7684\u4efb\u52a1\u4e0a\u4e0b\u6587\u3002 tool.delegateToAgent.param.agentName=\u76ee\u6807 Agent \u7684\u540d\u79f0\uff08\u7cbe\u786e\u5339\u914d\uff09 tool.delegateToAgent.param.task=\u8981\u59d4\u6d3e\u7684\u4efb\u52a1\u63cf\u8ff0\uff0c\u5fc5\u987b\u5305\u542b\u5b8c\u6574\u4e0a\u4e0b\u6587\u4fe1\u606f diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties index 07e2710a..f6e01bc7 100644 --- a/mateclaw-server/src/main/resources/messages_en.properties +++ b/mateclaw-server/src/main/resources/messages_en.properties @@ -43,6 +43,10 @@ tool.search.param.freshness=Time range filter: day (today), week (this week), mo tool.search.param.language=Language preference: zh-CN (Chinese), en (English) tool.search.param.count=Max results: 1-10, default 5 +tool.create_cron_job.desc=Create a scheduled task (cron job). Runs automatically at specified time and sends trigger message to current agent. Use 5-field cron: minute hour day month weekday. +tool.list_cron_jobs.desc=List all scheduled tasks with name, cron expression, next run time, and enabled status. +tool.toggle_cron_job.desc=Enable or disable a scheduled task by its job ID. +tool.delete_cron_job.desc=Delete a scheduled task. Requires user approval. tool.delegateToAgent.desc=Delegate a task to another Agent for multi-agent collaboration. Target Agent executes in an independent session and returns its final reply. Provide complete task context as the target cannot see current conversation history. tool.delegateToAgent.param.agentName=Target Agent name (exact match) tool.delegateToAgent.param.task=Task description with complete context information