This is an automated email from the ASF dual-hosted git repository.

gaoxingcun pushed a commit to branch feature/ai-sop-workflow
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit 4540fe2fe9971cf3d3b202d1f75fe4ecd5476bbb
Author: TJxiaobao <[email protected]>
AuthorDate: Mon Feb 2 23:11:39 2026 +0800

    feat: implement AI scheduled monitoring with full i18n support
    - Add AI scheduled task feature for periodic SOP execution and results push.
    - Implement schedule management UI with create, edit, delete, and toggle 
support.
    - Support dynamic report language based on user's system locale 
configuration.
    - Add database persistence and Flyway migrations for SOP schedule tasks.
    - Refactor i18n utility to support background threads and update 
translations.
---
 .../ai/controller/SopScheduleController.java       | 131 ++++++++++
 .../apache/hertzbeat/ai/dao/SopScheduleDao.java    |  61 +++++
 .../hertzbeat/ai/schedule/SopScheduleExecutor.java | 175 ++++++++++++++
 .../hertzbeat/ai/service/SopScheduleService.java   |  81 +++++++
 .../impl/ChatClientProviderServiceImpl.java        |  14 +-
 .../ai/service/impl/McpServerServiceImpl.java      |   5 +-
 .../ai/service/impl/SopScheduleServiceImpl.java    | 157 ++++++++++++
 .../hertzbeat/ai/sop/executor/LlmExecutor.java     |   2 +-
 .../hertzbeat/ai/sop/model/OutputConfig.java       |  10 +-
 .../apache/hertzbeat/ai/sop/model/SopResult.java   |   2 +-
 .../apache/hertzbeat/ai/tools/ScheduleTools.java   |  55 +++++
 .../hertzbeat/ai/tools/impl/ScheduleToolsImpl.java | 268 +++++++++++++++++++++
 .../ai/{sop/util => utils}/SopMessageUtil.java     |   7 +-
 .../src/main/resources/i18n/messages.properties    |  37 +++
 .../src/main/resources/i18n/messages_en.properties |  37 +++
 .../src/main/resources/i18n/messages_zh.properties |  37 +++
 .../src/main/resources/prompt/system-message.st    |  39 ++-
 .../src/main/resources/skills/daily_inspection.yml |   2 +-
 .../hertzbeat/common/entity/ai/SopSchedule.java    | 111 +++++++++
 .../db/migration/h2/V181__update_column.sql        |  18 ++
 .../db/migration/mysql/V181__update_column.sql     |  17 ++
 .../migration/postgresql/V181__update_column.sql   |  27 +++
 web-app/src/app/service/ai-chat.service.ts         |  65 ++++-
 .../shared/components/ai-chat/ai-chat.module.ts    |   8 +-
 .../shared/components/ai-chat/chat.component.html  | 221 ++++++++++++-----
 .../shared/components/ai-chat/chat.component.less  | 232 +++++++++++-------
 .../shared/components/ai-chat/chat.component.ts    | 204 +++++++++++++++-
 web-app/src/assets/i18n/en-US.json                 |  38 ++-
 web-app/src/assets/i18n/zh-CN.json                 |  38 ++-
 29 files changed, 1925 insertions(+), 174 deletions(-)

diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopScheduleController.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopScheduleController.java
new file mode 100644
index 0000000000..167f43866c
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/SopScheduleController.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import org.apache.hertzbeat.ai.service.SopScheduleService;
+import org.apache.hertzbeat.common.entity.ai.SopSchedule;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+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;
+
+/**
+ * REST Controller for managing scheduled SOP executions.
+ */
+@Tag(name = "SOP Schedule API", description = "Manage scheduled SOP 
executions")
+@RestController
+@RequestMapping("/api/ai/schedule")
+public class SopScheduleController {
+
+    private final SopScheduleService sopScheduleService;
+    private final org.apache.hertzbeat.ai.sop.registry.SkillRegistry 
skillRegistry;
+
+    @Autowired
+    public SopScheduleController(SopScheduleService sopScheduleService,
+                                  
org.apache.hertzbeat.ai.sop.registry.SkillRegistry skillRegistry) {
+        this.sopScheduleService = sopScheduleService;
+        this.skillRegistry = skillRegistry;
+    }
+
+    @Operation(summary = "Get all available SOP skills")
+    @GetMapping("/skills")
+    public ResponseEntity<Message<List<java.util.Map<String, String>>>> 
getAvailableSkills() {
+        List<java.util.Map<String, String>> skills = new 
java.util.ArrayList<>();
+        for (var skill : skillRegistry.getAllSkills()) {
+            java.util.Map<String, String> skillInfo = new 
java.util.HashMap<>();
+            skillInfo.put("name", skill.getName());
+            skillInfo.put("description", skill.getDescription());
+            skills.add(skillInfo);
+        }
+        return ResponseEntity.ok(Message.success(skills));
+    }
+
+    @Operation(summary = "Get all schedules for a conversation")
+    @GetMapping("/conversation/{conversationId}")
+    public ResponseEntity<Message<List<SopSchedule>>> 
getSchedulesByConversation(
+            @PathVariable Long conversationId) {
+        List<SopSchedule> schedules = 
sopScheduleService.getSchedulesByConversation(conversationId);
+        return ResponseEntity.ok(Message.success(schedules));
+    }
+
+    @Operation(summary = "Get a schedule by ID")
+    @GetMapping("/{id}")
+    public ResponseEntity<Message<SopSchedule>> getSchedule(@PathVariable Long 
id) {
+        SopSchedule schedule = sopScheduleService.getSchedule(id);
+        if (schedule == null) {
+            return ResponseEntity.ok(Message.fail((byte) 404, "Schedule not 
found"));
+        }
+        return ResponseEntity.ok(Message.success(schedule));
+    }
+
+    @Operation(summary = "Create a new scheduled SOP task")
+    @PostMapping
+    public ResponseEntity<Message<SopSchedule>> createSchedule(@RequestBody 
SopSchedule schedule) {
+        try {
+            SopSchedule created = sopScheduleService.createSchedule(schedule);
+            return ResponseEntity.ok(Message.success(created));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage()));
+        }
+    }
+
+    @Operation(summary = "Update an existing schedule")
+    @PutMapping("/{id}")
+    public ResponseEntity<Message<SopSchedule>> updateSchedule(
+            @PathVariable Long id, 
+            @RequestBody SopSchedule schedule) {
+        try {
+            schedule.setId(id);
+            SopSchedule updated = sopScheduleService.updateSchedule(schedule);
+            return ResponseEntity.ok(Message.success(updated));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage()));
+        }
+    }
+
+    @Operation(summary = "Delete a schedule")
+    @DeleteMapping("/{id}")
+    public ResponseEntity<Message<Void>> deleteSchedule(@PathVariable Long id) 
{
+        sopScheduleService.deleteSchedule(id);
+        return ResponseEntity.ok(Message.success());
+    }
+
+    @Operation(summary = "Toggle schedule enabled status")
+    @PutMapping("/{id}/toggle")
+    public ResponseEntity<Message<SopSchedule>> toggleSchedule(
+            @PathVariable Long id,
+            @RequestParam boolean enabled) {
+        try {
+            SopSchedule updated = sopScheduleService.toggleSchedule(id, 
enabled);
+            return ResponseEntity.ok(Message.success(updated));
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage()));
+        }
+    }
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/SopScheduleDao.java 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/SopScheduleDao.java
new file mode 100644
index 0000000000..08861591e6
--- /dev/null
+++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/dao/SopScheduleDao.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.dao;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import org.apache.hertzbeat.common.entity.ai.SopSchedule;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Repository for SopSchedule entity operations.
+ */
+@Repository
+public interface SopScheduleDao extends JpaRepository<SopSchedule, Long>, 
JpaSpecificationExecutor<SopSchedule> {
+
+    /**
+     * Find all schedules for a specific conversation.
+     * @param conversationId The conversation ID
+     * @return List of schedules
+     */
+    List<SopSchedule> findByConversationId(Long conversationId);
+
+    /**
+     * Find all enabled schedules that are due for execution.
+     * @param currentTime The current time to compare against
+     * @return List of due schedules
+     */
+    @Query("SELECT s FROM SopSchedule s WHERE s.enabled = true AND 
s.nextRunTime <= :currentTime")
+    List<SopSchedule> findDueSchedules(@Param("currentTime") LocalDateTime 
currentTime);
+
+    /**
+     * Find all enabled schedules.
+     * @return List of enabled schedules
+     */
+    List<SopSchedule> findByEnabledTrue();
+
+    /**
+     * Delete all schedules for a conversation.
+     * @param conversationId The conversation ID
+     */
+    void deleteByConversationId(Long conversationId);
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/schedule/SopScheduleExecutor.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/schedule/SopScheduleExecutor.java
new file mode 100644
index 0000000000..6e346f98c0
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/schedule/SopScheduleExecutor.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.schedule;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.dao.ChatMessageDao;
+import org.apache.hertzbeat.ai.service.SopScheduleService;
+import org.apache.hertzbeat.ai.sop.engine.SopEngine;
+import org.apache.hertzbeat.ai.sop.model.SopResult;
+import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
+import org.apache.hertzbeat.ai.utils.SopMessageUtil;
+import org.apache.hertzbeat.common.entity.ai.ChatMessage;
+import org.apache.hertzbeat.common.entity.ai.SopSchedule;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * Scheduled executor that checks for due SOP schedules and executes them.
+ * Runs every minute to check if any schedules are due for execution.
+ */
+@Slf4j
+@Component
+@EnableScheduling
+public class SopScheduleExecutor {
+
+    /**
+     * Role identifier for system push messages in conversations.
+     */
+    public static final String ROLE_SYSTEM_PUSH = "system_push";
+
+    private final SopScheduleService sopScheduleService;
+    private final SopEngine sopEngine;
+    private final SkillRegistry skillRegistry;
+    private final ChatMessageDao chatMessageDao;
+
+    @Autowired
+    public SopScheduleExecutor(SopScheduleService sopScheduleService,
+                               @Lazy SopEngine sopEngine,
+                               @Lazy SkillRegistry skillRegistry,
+                               ChatMessageDao chatMessageDao) {
+        this.sopScheduleService = sopScheduleService;
+        this.sopEngine = sopEngine;
+        this.skillRegistry = skillRegistry;
+        this.chatMessageDao = chatMessageDao;
+    }
+
+    /**
+     * Check for due schedules every minute and execute them.
+     */
+    @Scheduled(fixedRate = 60000) // Every minute
+    public void checkAndExecuteDueSchedules() {
+        try {
+            List<SopSchedule> dueSchedules = 
sopScheduleService.getDueSchedules();
+            
+            if (dueSchedules.isEmpty()) {
+                return;
+            }
+            
+            log.info("Found {} due schedules to execute", dueSchedules.size());
+            
+            for (SopSchedule schedule : dueSchedules) {
+                executeSchedule(schedule);
+            }
+        } catch (Exception e) {
+            log.error("Error checking due schedules", e);
+        }
+    }
+
+    /**
+     * Execute a single scheduled SOP and push result to conversation.
+     */
+    private void executeSchedule(SopSchedule schedule) {
+        log.info("Executing scheduled SOP {} for conversation {}", 
+                schedule.getSopName(), schedule.getConversationId());
+        
+        try {
+            // Check if skill exists
+            var definition = skillRegistry.getSkill(schedule.getSopName());
+            if (definition == null) {
+                log.warn("Skill {} not found, skipping schedule {}", 
+                        schedule.getSopName(), schedule.getId());
+                return;
+            }
+            
+            // Parse parameters
+            Map<String, Object> params = new HashMap<>();
+            if (schedule.getSopParams() != null && 
!schedule.getSopParams().isEmpty()) {
+                try {
+                    params = JsonUtil.fromJson(schedule.getSopParams(), 
Map.class);
+                } catch (Exception e) {
+                    log.warn("Failed to parse SOP params: {}", 
schedule.getSopParams());
+                }
+            }
+            
+            // Execute SOP
+            SopResult result = sopEngine.executeSync(definition, params);
+            
+            // Create push message
+            String messageContent = formatPushMessage(schedule, result);
+            
+            // Save message to conversation
+            ChatMessage pushMessage = ChatMessage.builder()
+                    .conversationId(schedule.getConversationId())
+                    .role(ROLE_SYSTEM_PUSH)
+                    .content(messageContent)
+                    .build();
+            
+            chatMessageDao.save(pushMessage);
+            
+            log.info("Successfully pushed SOP result to conversation {}", 
+                    schedule.getConversationId());
+            
+        } catch (Exception e) {
+            log.error("Failed to execute scheduled SOP {} for conversation 
{}", 
+                    schedule.getSopName(), schedule.getConversationId(), e);
+            
+            // Still save an error message
+            String errorContent = 
SopMessageUtil.getMessage("schedule.push.error.prefix") + " " + 
schedule.getSopName() 
+                    + "\n\n" + 
SopMessageUtil.getMessage("schedule.push.error.label") + " " + e.getMessage();
+            ChatMessage errorMessage = ChatMessage.builder()
+                    .conversationId(schedule.getConversationId())
+                    .role(ROLE_SYSTEM_PUSH)
+                    .content(errorContent)
+                    .build();
+            chatMessageDao.save(errorMessage);
+            
+        } finally {
+            // Update schedule times
+            sopScheduleService.updateAfterExecution(schedule.getId());
+        }
+    }
+
+    /**
+     * Format the push message content with SOP result.
+     */
+    private String formatPushMessage(SopSchedule schedule, SopResult result) {
+        StringBuilder sb = new StringBuilder();
+        
sb.append(SopMessageUtil.getMessage("schedule.push.report.title")).append("\n\n");
+        
sb.append(SopMessageUtil.getMessage("schedule.push.report.skill")).append(": 
").append(schedule.getSopName()).append("\n");
+        
sb.append(SopMessageUtil.getMessage("schedule.push.report.time")).append(": 
").append(LocalDateTime.now()).append("\n\n");
+        sb.append("---\n\n");
+        
+        if ("SUCCESS".equals(result.getStatus())) {
+            sb.append(result.getContent());
+        } else {
+            
sb.append(SopMessageUtil.getMessage("schedule.push.report.failed")).append("\n\n");
+            sb.append(result.getError());
+        }
+        
+        return sb.toString();
+    }
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/SopScheduleService.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/SopScheduleService.java
new file mode 100644
index 0000000000..3f04938bf6
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/SopScheduleService.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.service;
+
+import java.util.List;
+import org.apache.hertzbeat.common.entity.ai.SopSchedule;
+
+/**
+ * Service interface for managing scheduled SOP executions.
+ */
+public interface SopScheduleService {
+
+    /**
+     * Create a new scheduled SOP task.
+     * @param schedule The schedule configuration
+     * @return The created schedule with generated ID
+     */
+    SopSchedule createSchedule(SopSchedule schedule);
+
+    /**
+     * Update an existing schedule.
+     * @param schedule The updated schedule
+     * @return The updated schedule
+     */
+    SopSchedule updateSchedule(SopSchedule schedule);
+
+    /**
+     * Delete a schedule by ID.
+     * @param id The schedule ID
+     */
+    void deleteSchedule(Long id);
+
+    /**
+     * Get a schedule by ID.
+     * @param id The schedule ID
+     * @return The schedule or null if not found
+     */
+    SopSchedule getSchedule(Long id);
+
+    /**
+     * Get all schedules for a conversation.
+     * @param conversationId The conversation ID
+     * @return List of schedules
+     */
+    List<SopSchedule> getSchedulesByConversation(Long conversationId);
+
+    /**
+     * Toggle the enabled status of a schedule.
+     * @param id The schedule ID
+     * @param enabled The new enabled status
+     * @return The updated schedule
+     */
+    SopSchedule toggleSchedule(Long id, boolean enabled);
+
+    /**
+     * Get all schedules that are due for execution.
+     * @return List of due schedules
+     */
+    List<SopSchedule> getDueSchedules();
+
+    /**
+     * Update the execution times after a schedule runs.
+     * @param id The schedule ID
+     */
+    void updateAfterExecution(Long id);
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
index 67211c1c02..edd8bd4ea8 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
@@ -58,6 +58,7 @@ import java.util.List;
 public class ChatClientProviderServiceImpl implements 
ChatClientProviderService {
 
     private static final String SKILLS_PLACEHOLDER = 
"{dynamically_injected_skills_list}";
+    private static final String CONVERSATION_ID_PLACEHOLDER = 
"{current_conversation_id}";
 
     private final ApplicationContext applicationContext;
 
@@ -106,8 +107,9 @@ public class ChatClientProviderServiceImpl implements 
ChatClientProviderService
 
             log.info("Starting streaming chat for conversation: {}", 
context.getConversationId());
 
-            // Build system prompt with dynamic skills list
-            String systemPrompt = buildSystemPrompt();
+            // Build system prompt with dynamic skills list and conversation ID
+            // The conversationId is injected into the prompt so AI can pass 
it to schedule tools
+            String systemPrompt = 
buildSystemPrompt(context.getConversationId());
 
             return chatClient.prompt()
                     .messages(messages)
@@ -125,13 +127,15 @@ public class ChatClientProviderServiceImpl implements 
ChatClientProviderService
     }
 
     /**
-     * Build the system prompt with dynamically injected skills list.
+     * Build the system prompt with dynamically injected skills list and 
conversation ID.
      */
-    private String buildSystemPrompt() {
+    private String buildSystemPrompt(Long conversationId) {
         try {
             String template = 
systemResource.getContentAsString(StandardCharsets.UTF_8);
             String skillsList = generateSkillsList();
-            return template.replace(SKILLS_PLACEHOLDER, skillsList);
+            return template
+                    .replace(SKILLS_PLACEHOLDER, skillsList)
+                    .replace(CONVERSATION_ID_PLACEHOLDER, 
String.valueOf(conversationId));
         } catch (IOException e) {
             log.error("Failed to read system prompt template: {}", 
e.getMessage());
             return "";
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
index 02fc30c004..23c0dda3c1 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/McpServerServiceImpl.java
@@ -23,6 +23,7 @@ import org.apache.hertzbeat.ai.tools.AlertDefineTools;
 import org.apache.hertzbeat.ai.tools.AlertTools;
 import org.apache.hertzbeat.ai.tools.MetricsTools;
 import org.apache.hertzbeat.ai.tools.MonitorTools;
+import org.apache.hertzbeat.ai.tools.ScheduleTools;
 import org.apache.hertzbeat.ai.tools.SkillTools;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -48,6 +49,8 @@ public class McpServerServiceImpl implements McpServerService 
{
     private AlertDefineTools alertDefineTools;
     @Autowired
     private SkillTools skillTools;
+    @Autowired
+    private ScheduleTools scheduleTools;
 
     @Bean
     public ToolCallbackProvider hertzbeatTools() {
@@ -55,7 +58,7 @@ public class McpServerServiceImpl implements McpServerService 
{
         // AI should use Skills (via skillTools) to perform database 
diagnostics.
         return MethodToolCallbackProvider.builder()
                 .toolObjects(monitorTools, alertTools, alertDefineTools, 
metricsTools, 
-                             skillTools)
+                             skillTools, scheduleTools)
                 .build();
     }
 }
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/SopScheduleServiceImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/SopScheduleServiceImpl.java
new file mode 100644
index 0000000000..88c8d0a23b
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/SopScheduleServiceImpl.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.service.impl;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.dao.SopScheduleDao;
+import org.apache.hertzbeat.ai.service.SopScheduleService;
+import org.apache.hertzbeat.common.entity.ai.SopSchedule;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.support.CronExpression;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Implementation of SopScheduleService for managing scheduled SOP executions.
+ */
+@Slf4j
+@Service
+public class SopScheduleServiceImpl implements SopScheduleService {
+
+    private final SopScheduleDao sopScheduleDao;
+
+    @Autowired
+    public SopScheduleServiceImpl(SopScheduleDao sopScheduleDao) {
+        this.sopScheduleDao = sopScheduleDao;
+    }
+
+    @Override
+    @Transactional
+    public SopSchedule createSchedule(SopSchedule schedule) {
+        // Validate cron expression
+        validateCronExpression(schedule.getCronExpression());
+        
+        // Calculate next run time
+        
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
+        schedule.setEnabled(schedule.getEnabled() != null ? 
schedule.getEnabled() : true);
+        
+        SopSchedule saved = sopScheduleDao.save(schedule);
+        log.info("Created schedule {} for conversation {} with SOP {}", 
+                saved.getId(), saved.getConversationId(), saved.getSopName());
+        return saved;
+    }
+
+    @Override
+    @Transactional
+    public SopSchedule updateSchedule(SopSchedule schedule) {
+        SopSchedule existing = sopScheduleDao.findById(schedule.getId())
+                .orElseThrow(() -> new IllegalArgumentException("Schedule not 
found: " + schedule.getId()));
+        
+        // Update fields
+        existing.setSopName(schedule.getSopName());
+        existing.setSopParams(schedule.getSopParams());
+        
+        // If cron expression changed, recalculate next run time
+        if 
(!existing.getCronExpression().equals(schedule.getCronExpression())) {
+            validateCronExpression(schedule.getCronExpression());
+            existing.setCronExpression(schedule.getCronExpression());
+            
existing.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
+        }
+        
+        if (schedule.getEnabled() != null) {
+            existing.setEnabled(schedule.getEnabled());
+        }
+        
+        return sopScheduleDao.save(existing);
+    }
+
+    @Override
+    @Transactional
+    public void deleteSchedule(Long id) {
+        log.info("Deleting schedule {}", id);
+        sopScheduleDao.deleteById(id);
+    }
+
+    @Override
+    public SopSchedule getSchedule(Long id) {
+        return sopScheduleDao.findById(id).orElse(null);
+    }
+
+    @Override
+    public List<SopSchedule> getSchedulesByConversation(Long conversationId) {
+        return sopScheduleDao.findByConversationId(conversationId);
+    }
+
+    @Override
+    @Transactional
+    public SopSchedule toggleSchedule(Long id, boolean enabled) {
+        SopSchedule schedule = sopScheduleDao.findById(id)
+                .orElseThrow(() -> new IllegalArgumentException("Schedule not 
found: " + id));
+        
+        schedule.setEnabled(enabled);
+        
+        // If enabling, recalculate next run time
+        if (enabled) {
+            
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
+        }
+        
+        log.info("Schedule {} {} ", id, enabled ? "enabled" : "disabled");
+        return sopScheduleDao.save(schedule);
+    }
+
+    @Override
+    public List<SopSchedule> getDueSchedules() {
+        return sopScheduleDao.findDueSchedules(LocalDateTime.now());
+    }
+
+    @Override
+    @Transactional
+    public void updateAfterExecution(Long id) {
+        SopSchedule schedule = sopScheduleDao.findById(id).orElse(null);
+        if (schedule == null) {
+            return;
+        }
+        
+        schedule.setLastRunTime(LocalDateTime.now());
+        
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
+        sopScheduleDao.save(schedule);
+        
+        log.debug("Updated schedule {} - Last run: {}, Next run: {}", 
+                id, schedule.getLastRunTime(), schedule.getNextRunTime());
+    }
+
+    private void validateCronExpression(String cronExpression) {
+        try {
+            CronExpression.parse(cronExpression);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException("Invalid cron expression: " + 
cronExpression, e);
+        }
+    }
+
+    private LocalDateTime calculateNextRunTime(String cronExpression) {
+        try {
+            CronExpression cron = CronExpression.parse(cronExpression);
+            return cron.next(LocalDateTime.now());
+        } catch (Exception e) {
+            log.error("Failed to calculate next run time for cron: {}", 
cronExpression, e);
+            return null;
+        }
+    }
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java
index 0e58c385c6..1efaa2e7bf 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/executor/LlmExecutor.java
@@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
 import org.apache.hertzbeat.ai.service.ChatClientProviderService;
 import org.apache.hertzbeat.ai.sop.model.SopStep;
-import org.apache.hertzbeat.ai.sop.util.SopMessageUtil;
+import org.apache.hertzbeat.ai.utils.SopMessageUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java
index e236d444e9..c99bd93842 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/OutputConfig.java
@@ -59,11 +59,17 @@ public class OutputConfig {
     private String language;
     
     /**
-     * Get language code, default to zh (Chinese)
+     * Get language code.
+     * If not specified in skill config, uses system default locale (from 
user's SystemConfig).
      */
     public String getLanguageCode() {
         if (language == null || language.isEmpty()) {
-            return "zh";
+            // Use system default locale (set by user's SystemConfig)
+            java.util.Locale defaultLocale = java.util.Locale.getDefault();
+            if (defaultLocale.getLanguage().equals("en")) {
+                return "en";
+            }
+            return "zh"; // Default to Chinese for non-English locales
         }
         return language.toLowerCase();
     }
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java
index 1facf238a5..13ad85b7a8 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/model/SopResult.java
@@ -25,7 +25,7 @@ import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.apache.hertzbeat.ai.sop.util.SopMessageUtil;
+import org.apache.hertzbeat.ai.utils.SopMessageUtil;
 
 /**
  * Unified SOP execution result.
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/ScheduleTools.java 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/ScheduleTools.java
new file mode 100644
index 0000000000..eb4154d40e
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/ScheduleTools.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.tools;
+
+/**
+ * Interface for AI Schedule management tools.
+ * Allows AI to create, list, and manage scheduled tasks via natural language.
+ */
+public interface ScheduleTools {
+
+    /**
+     * Create a scheduled task to execute a skill periodically.
+     * @param skillName Name of the skill to schedule (e.g., daily_inspection)
+     * @param cronExpression Cron expression for scheduling (6-digit Spring 
format)
+     * @param description Human-readable description of the schedule
+     * @return Confirmation message with schedule details
+     */
+    String createSchedule(String skillName, String cronExpression, String 
description);
+
+    /**
+     * List all scheduled tasks for the current conversation.
+     * @return List of scheduled tasks with their status
+     */
+    String listSchedules();
+
+    /**
+     * Delete a scheduled task by ID.
+     * @param scheduleId ID of the schedule to delete
+     * @return Confirmation message
+     */
+    String deleteSchedule(Long scheduleId);
+
+    /**
+     * Enable or disable a scheduled task.
+     * @param scheduleId ID of the schedule
+     * @param enabled Whether to enable or disable
+     * @return Confirmation message
+     */
+    String toggleSchedule(Long scheduleId, boolean enabled);
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/ScheduleToolsImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/ScheduleToolsImpl.java
new file mode 100644
index 0000000000..f82260c4ee
--- /dev/null
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/ScheduleToolsImpl.java
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.ai.tools.impl;
+
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.ai.service.SopScheduleService;
+import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
+import org.apache.hertzbeat.ai.utils.SopMessageUtil;
+import org.apache.hertzbeat.ai.tools.ScheduleTools;
+import org.apache.hertzbeat.common.entity.ai.SopSchedule;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+/**
+ * Implementation of ScheduleTools for AI-driven schedule management.
+ * Allows AI to create and manage scheduled tasks through natural language.
+ * 
+ * The conversationId is injected into the System Prompt so AI can pass it
+ * when calling these tools.
+ */
+@Slf4j
+@Service
+public class ScheduleToolsImpl implements ScheduleTools {
+
+    private static final DateTimeFormatter TIME_FORMATTER = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private final SopScheduleService scheduleService;
+    private final SkillRegistry skillRegistry;
+
+    @Autowired
+    public ScheduleToolsImpl(@Lazy SopScheduleService scheduleService, 
+                             @Lazy SkillRegistry skillRegistry) {
+        this.scheduleService = scheduleService;
+        this.skillRegistry = skillRegistry;
+    }
+
+    @Override
+    @Tool(name = "createScheduleForConversation",
+          description = "Create a scheduled task to automatically execute a 
skill at specified times. "
+                  + "IMPORTANT: You must pass the conversationId from the 
system context (shown as 'Current Conversation ID' in the prompt). "
+                  + "The cron expression should be in 6-digit Spring format: 
'seconds minutes hours dayOfMonth month dayOfWeek'. "
+                  + "Common examples: '0 0 9 * * ?' (daily at 9:00), '0 30 8 * 
* MON-FRI' (weekdays at 8:30).")
+    public String createSchedule(
+            @ToolParam(description = "Name of the skill to schedule (e.g., 
'daily_inspection', 'mysql_slow_query_diagnosis')", 
+                       required = true) String skillName,
+            @ToolParam(description = "Cron expression in 6-digit Spring 
format. Examples: '0 0 9 * * ?' for daily 9am", 
+                       required = true) String cronExpression,
+            @ToolParam(description = "Human-readable description of what this 
schedule does", 
+                       required = false) String description) {
+        // This method signature is kept for interface compatibility
+        // AI should use createScheduleWithConversation instead
+        return 
SopMessageUtil.getMessage("schedule.error.use.with.conversation");
+    }
+
+    /**
+     * Create schedule with explicit conversationId - the primary method AI 
should call.
+     */
+    @Tool(name = "createScheduleWithConversation",
+          description = "Create a scheduled task for a specific conversation. "
+                  + "Use the conversationId from the system context. "
+                  + "The cron expression should be in 6-digit Spring format.")
+    public String createScheduleWithConversation(
+            @ToolParam(description = "Conversation ID from the system 
context", required = true) Long conversationId,
+            @ToolParam(description = "Name of the skill to schedule (e.g., 
'daily_inspection')", required = true) String skillName,
+            @ToolParam(description = "Cron expression in Spring format (e.g., 
'0 0 9 * * ?')", required = true) String cronExpression,
+            @ToolParam(description = "Description of the schedule", required = 
false) String description) {
+
+        log.info("AI creating schedule: conversationId={}, skill={}, cron={}, 
desc={}", 
+                 conversationId, skillName, cronExpression, description);
+
+        // Validate skill exists
+        if (skillRegistry.getSkill(skillName) == null) {
+            String available = String.join(", ", 
+                    skillRegistry.getAllSkills().stream()
+                            .map(s -> s.getName())
+                            .toList());
+            return SopMessageUtil.getMessage("schedule.error.skill.not.found", 
+                    new Object[]{skillName, available}, null);
+        }
+
+        if (conversationId == null || conversationId <= 0) {
+            return 
SopMessageUtil.getMessage("schedule.error.invalid.conversation");
+        }
+
+        try {
+            // Check for duplicate schedule (same skill + cron expression)
+            List<SopSchedule> existing = 
scheduleService.getSchedulesByConversation(conversationId);
+            boolean duplicate = existing.stream()
+                    .anyMatch(s -> s.getSopName().equals(skillName) 
+                                && 
s.getCronExpression().equals(cronExpression));
+            if (duplicate) {
+                return SopMessageUtil.getMessage("schedule.create.duplicate", 
+                        new Object[]{skillName, cronExpression}, null)
+                        + "\n\n" + 
SopMessageUtil.getMessage("schedule.create.duplicate.hint");
+            }
+
+            SopSchedule schedule = new SopSchedule();
+            schedule.setConversationId(conversationId);
+            schedule.setSopName(skillName);
+            schedule.setCronExpression(cronExpression);
+            schedule.setEnabled(true);
+
+            SopSchedule created = scheduleService.createSchedule(schedule);
+
+            StringBuilder response = new StringBuilder();
+            
response.append(SopMessageUtil.getMessage("schedule.create.success")).append("\n\n");
+            
response.append("**").append(SopMessageUtil.getMessage("schedule.detail.title")).append("**:\n");
+            response.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.skill")).append("**: 
").append(skillName).append("\n");
+            response.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.cron")).append("**: 
").append(cronExpression).append("\n");
+            response.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.id")).append("**: 
").append(created.getId()).append("\n");
+            response.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.status")).append("**: 
").append(SopMessageUtil.getMessage("schedule.detail.enabled")).append("\n");
+            
+            if (created.getNextRunTime() != null) {
+                response.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.next.run")).append("**: 
").append(created.getNextRunTime().format(TIME_FORMATTER)).append("\n");
+            }
+
+            if (description != null && !description.isEmpty()) {
+                response.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.description")).append("**:
 ").append(description).append("\n");
+            }
+
+            
response.append("\n").append(SopMessageUtil.getMessage("schedule.create.effective"));
+
+            return response.toString();
+
+        } catch (IllegalArgumentException e) {
+            return SopMessageUtil.getMessage("schedule.error.create.failed") + 
": " + e.getMessage();
+        } catch (Exception e) {
+            log.error("Failed to create schedule", e);
+            return SopMessageUtil.getMessage("schedule.error.create.failed") + 
": " + e.getMessage();
+        }
+    }
+
+    @Override
+    @Tool(name = "listSchedulesForConversation",
+          description = "List all scheduled tasks for a specific conversation. 
"
+                  + "Use the conversationId from the system context.")
+    public String listSchedules() {
+        // This method signature is kept for interface compatibility
+        return 
SopMessageUtil.getMessage("schedule.error.use.with.conversation");
+    }
+
+    @Tool(name = "listSchedulesWithConversation",
+          description = "List all scheduled tasks for a specific 
conversation.")
+    public String listSchedulesWithConversation(
+            @ToolParam(description = "Conversation ID from the system 
context", required = true) Long conversationId) {
+        
+        log.info("AI listing schedules for conversation: {}", conversationId);
+
+        if (conversationId == null || conversationId <= 0) {
+            return 
SopMessageUtil.getMessage("schedule.error.invalid.conversation");
+        }
+
+        try {
+            List<SopSchedule> schedules = 
scheduleService.getSchedulesByConversation(conversationId);
+
+            if (schedules.isEmpty()) {
+                return SopMessageUtil.getMessage("schedule.list.empty") + 
"\n\n" 
+                        + SopMessageUtil.getMessage("schedule.list.hint");
+            }
+
+            StringBuilder sb = new StringBuilder();
+            
sb.append("**").append(SopMessageUtil.getMessage("schedule.list.title")).append("**
 (")
+              .append(SopMessageUtil.getMessage("schedule.list.count", new 
Object[]{schedules.size()}, null))
+              .append("):\n\n");
+
+            for (SopSchedule schedule : schedules) {
+                String statusLabel = schedule.getEnabled() 
+                        ? SopMessageUtil.getMessage("schedule.detail.enabled")
+                        : 
SopMessageUtil.getMessage("schedule.detail.disabled");
+                
+                sb.append("### ").append(schedule.getId()).append(". 
").append(schedule.getSopName());
+                sb.append(" (").append(statusLabel).append(")\n");
+                sb.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.cron")).append("**: 
`").append(schedule.getCronExpression()).append("`\n");
+                sb.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.status")).append("**: 
").append(statusLabel).append("\n");
+                
+                if (schedule.getLastRunTime() != null) {
+                    sb.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.last.run")).append("**: 
").append(schedule.getLastRunTime().format(TIME_FORMATTER)).append("\n");
+                }
+                if (schedule.getNextRunTime() != null) {
+                    sb.append("- 
**").append(SopMessageUtil.getMessage("schedule.detail.next.run")).append("**: 
").append(schedule.getNextRunTime().format(TIME_FORMATTER)).append("\n");
+                }
+                sb.append("\n");
+            }
+
+            return sb.toString();
+
+        } catch (Exception e) {
+            log.error("Failed to list schedules", e);
+            return "Error: " + e.getMessage();
+        }
+    }
+
+    @Override
+    @Tool(name = "deleteSchedule",
+          description = "Delete a scheduled task by its ID. Use 
listSchedulesForConversation first to find the task ID.")
+    public String deleteSchedule(
+            @ToolParam(description = "ID of the schedule to delete", required 
= true) Long scheduleId) {
+
+        log.info("AI deleting schedule: {}", scheduleId);
+
+        try {
+            SopSchedule schedule = scheduleService.getSchedule(scheduleId);
+            if (schedule == null) {
+                return SopMessageUtil.getMessage("schedule.error.not.found", 
new Object[]{scheduleId}, null);
+            }
+
+            String skillName = schedule.getSopName();
+            scheduleService.deleteSchedule(scheduleId);
+
+            return SopMessageUtil.getMessage("schedule.delete.success", new 
Object[]{scheduleId, skillName}, null);
+
+        } catch (Exception e) {
+            log.error("Failed to delete schedule {}", scheduleId, e);
+            return "Error: " + e.getMessage();
+        }
+    }
+
+    @Override
+    @Tool(name = "toggleSchedule",
+          description = "Enable or disable a scheduled task. Use 
listSchedulesForConversation first to find the task ID.")
+    public String toggleSchedule(
+            @ToolParam(description = "ID of the schedule to toggle", required 
= true) Long scheduleId,
+            @ToolParam(description = "true to enable, false to disable", 
required = true) boolean enabled) {
+
+        log.info("AI toggling schedule {} to enabled={}", scheduleId, enabled);
+
+        try {
+            SopSchedule schedule = scheduleService.getSchedule(scheduleId);
+            if (schedule == null) {
+                return SopMessageUtil.getMessage("schedule.error.not.found", 
new Object[]{scheduleId}, null);
+            }
+
+            SopSchedule updated = scheduleService.toggleSchedule(scheduleId, 
enabled);
+
+            String messageKey = enabled ? "schedule.toggle.enabled" : 
"schedule.toggle.disabled";
+            String statusLabel = enabled 
+                    ? SopMessageUtil.getMessage("schedule.detail.enabled")
+                    : SopMessageUtil.getMessage("schedule.detail.disabled");
+            
+            return SopMessageUtil.getMessage(messageKey, new 
Object[]{scheduleId, updated.getSopName()}, null)
+                    + "\n\n" + 
SopMessageUtil.getMessage("schedule.toggle.status") + ": " + statusLabel;
+
+        } catch (Exception e) {
+            log.error("Failed to toggle schedule {}", scheduleId, e);
+            return "Error: " + e.getMessage();
+        }
+    }
+}
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/util/SopMessageUtil.java
 b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/SopMessageUtil.java
similarity index 92%
rename from 
hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/util/SopMessageUtil.java
rename to 
hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/SopMessageUtil.java
index 9b38a48441..9d11457e87 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/sop/util/SopMessageUtil.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/utils/SopMessageUtil.java
@@ -15,12 +15,11 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.ai.sop.util;
+package org.apache.hertzbeat.ai.utils;
 
 import java.util.Locale;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.MessageSource;
-import org.springframework.context.i18n.LocaleContextHolder;
 import org.springframework.stereotype.Component;
 
 /**
@@ -66,7 +65,9 @@ public class SopMessageUtil {
         } else if ("zh".equalsIgnoreCase(language) || 
"chinese".equalsIgnoreCase(language)) {
             locale = Locale.CHINESE;
         } else {
-            locale = LocaleContextHolder.getLocale();
+            // Use system default locale (set by user's SystemConfig)
+            // instead of LocaleContextHolder which only works in HTTP request 
context
+            locale = Locale.getDefault();
         }
         
         try {
diff --git a/hertzbeat-ai/src/main/resources/i18n/messages.properties 
b/hertzbeat-ai/src/main/resources/i18n/messages.properties
index 7a97ff01cf..fabab9ecc8 100644
--- a/hertzbeat-ai/src/main/resources/i18n/messages.properties
+++ b/hertzbeat-ai/src/main/resources/i18n/messages.properties
@@ -14,3 +14,40 @@ sop.result.operation.success=操作已完成
 
 # LLM Language Instructions
 sop.llm.language.instruction=重要:请使用中文回复。
+
+# Schedule Messages
+schedule.create.success=已成功创建定时任务!
+schedule.create.duplicate=该定时任务已存在(技能: {0}, 执行时间: {1}),无需重复创建。
+schedule.create.duplicate.hint=如需创建不同时间的任务,请指定不同的执行时间。
+schedule.detail.title=任务详情
+schedule.detail.skill=技能
+schedule.detail.cron=执行时间
+schedule.detail.id=任务ID
+schedule.detail.status=状态
+schedule.detail.enabled=已启用
+schedule.detail.disabled=已停用
+schedule.detail.next.run=下次执行
+schedule.detail.last.run=上次执行
+schedule.detail.description=描述
+schedule.create.effective=任务已创建并立即生效,系统将在设定时间自动执行。
+schedule.list.title=当前定时任务列表
+schedule.list.count=共 {0} 个
+schedule.list.empty=当前对话没有定时任务。
+schedule.list.hint=您可以说「每天早上9点执行每日巡检」来创建定时任务。
+schedule.delete.success=已删除定时任务 #{0} ({1})
+schedule.toggle.enabled=已启用定时任务 #{0} ({1})
+schedule.toggle.disabled=已停用定时任务 #{0} ({1})
+schedule.toggle.status=当前状态
+schedule.error.skill.not.found=技能 ''{0}'' 不存在。可用技能: {1}
+schedule.error.invalid.conversation=无效的会话ID,请从系统上下文传递正确的conversationId。
+schedule.error.not.found=未找到ID为 {0} 的定时任务。
+schedule.error.create.failed=创建定时任务失败
+schedule.error.use.with.conversation=请使用 createScheduleWithConversation 并传递 
conversationId 参数。
+
+# Schedule Push Messages
+schedule.push.error.prefix=定时任务执行失败:
+schedule.push.error.label=错误:
+schedule.push.report.title=**定时任务执行报告**
+schedule.push.report.skill=**技能**
+schedule.push.report.time=**执行时间**
+schedule.push.report.failed=**执行失败**
diff --git a/hertzbeat-ai/src/main/resources/i18n/messages_en.properties 
b/hertzbeat-ai/src/main/resources/i18n/messages_en.properties
index f358ade8e1..7e943929b5 100644
--- a/hertzbeat-ai/src/main/resources/i18n/messages_en.properties
+++ b/hertzbeat-ai/src/main/resources/i18n/messages_en.properties
@@ -14,3 +14,40 @@ sop.result.operation.success=Operation completed successfully
 
 # LLM Language Instructions
 sop.llm.language.instruction=IMPORTANT: Please respond in English.
+
+# Schedule Messages
+schedule.create.success=Schedule created successfully!
+schedule.create.duplicate=This schedule already exists (skill: {0}, cron: 
{1}). No need to create a duplicate.
+schedule.create.duplicate.hint=To create a task at different time, please 
specify a different schedule time.
+schedule.detail.title=Task Details
+schedule.detail.skill=Skill
+schedule.detail.cron=Schedule Time
+schedule.detail.id=Task ID
+schedule.detail.status=Status
+schedule.detail.enabled=Enabled
+schedule.detail.disabled=Disabled
+schedule.detail.next.run=Next Run
+schedule.detail.last.run=Last Run
+schedule.detail.description=Description
+schedule.create.effective=Task created and effective immediately. The system 
will execute it at the scheduled time.
+schedule.list.title=Scheduled Tasks List
+schedule.list.count=Total: {0}
+schedule.list.empty=No scheduled tasks in current conversation.
+schedule.list.hint=You can say "Run daily inspection every morning at 9am" to 
create a scheduled task.
+schedule.delete.success=Deleted schedule #{0} ({1})
+schedule.toggle.enabled=Enabled schedule #{0} ({1})
+schedule.toggle.disabled=Disabled schedule #{0} ({1})
+schedule.toggle.status=Current Status
+schedule.error.skill.not.found=Skill ''{0}'' not found. Available skills: {1}
+schedule.error.invalid.conversation=Invalid conversation ID. Please pass the 
correct conversationId from system context.
+schedule.error.not.found=Schedule with ID {0} not found.
+schedule.error.create.failed=Failed to create schedule
+schedule.error.use.with.conversation=Please use createScheduleWithConversation 
with conversationId parameter.
+
+# Schedule Push Messages
+schedule.push.error.prefix=Scheduled task execution failed:
+schedule.push.error.label=Error:
+schedule.push.report.title=**Scheduled Task Execution Report**
+schedule.push.report.skill=**Skill**
+schedule.push.report.time=**Execution Time**
+schedule.push.report.failed=**Execution Failed**
diff --git a/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties 
b/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties
index 56c8232fc8..ea2789727e 100644
--- a/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties
+++ b/hertzbeat-ai/src/main/resources/i18n/messages_zh.properties
@@ -14,3 +14,40 @@ sop.result.operation.success=操作已完成
 
 # LLM Language Instructions
 sop.llm.language.instruction=重要:请使用中文回复。
+
+# Schedule Messages
+schedule.create.success=已成功创建定时任务!
+schedule.create.duplicate=该定时任务已存在(技能: {0}, 执行时间: {1}),无需重复创建。
+schedule.create.duplicate.hint=如需创建不同时间的任务,请指定不同的执行时间。
+schedule.detail.title=任务详情
+schedule.detail.skill=技能
+schedule.detail.cron=执行时间
+schedule.detail.id=任务ID
+schedule.detail.status=状态
+schedule.detail.enabled=已启用
+schedule.detail.disabled=已停用
+schedule.detail.next.run=下次执行
+schedule.detail.last.run=上次执行
+schedule.detail.description=描述
+schedule.create.effective=任务已创建并立即生效,系统将在设定时间自动执行。
+schedule.list.title=当前定时任务列表
+schedule.list.count=共 {0} 个
+schedule.list.empty=当前对话没有定时任务。
+schedule.list.hint=您可以说「每天早上9点执行每日巡检」来创建定时任务。
+schedule.delete.success=已删除定时任务 #{0} ({1})
+schedule.toggle.enabled=已启用定时任务 #{0} ({1})
+schedule.toggle.disabled=已停用定时任务 #{0} ({1})
+schedule.toggle.status=当前状态
+schedule.error.skill.not.found=技能 ''{0}'' 不存在。可用技能: {1}
+schedule.error.invalid.conversation=无效的会话ID,请从系统上下文传递正确的conversationId。
+schedule.error.not.found=未找到ID为 {0} 的定时任务。
+schedule.error.create.failed=创建定时任务失败
+schedule.error.use.with.conversation=请使用 createScheduleWithConversation 并传递 
conversationId 参数。
+
+# Schedule Push Messages
+schedule.push.error.prefix=定时任务执行失败:
+schedule.push.error.label=错误:
+schedule.push.report.title=**定时任务执行报告**
+schedule.push.report.skill=**技能**
+schedule.push.report.time=**执行时间**
+schedule.push.report.failed=**执行失败**
diff --git a/hertzbeat-ai/src/main/resources/prompt/system-message.st 
b/hertzbeat-ai/src/main/resources/prompt/system-message.st
index 3660af46ed..68463361e0 100644
--- a/hertzbeat-ai/src/main/resources/prompt/system-message.st
+++ b/hertzbeat-ai/src/main/resources/prompt/system-message.st
@@ -53,6 +53,38 @@ You have access to automated skills that can perform complex 
multi-step operatio
 
 If no skill matches, use individual tools to assist the user.
 
+### Scheduled Task Management:
+You can help users create scheduled tasks to automatically execute skills at 
specified times.
+
+**Current Conversation ID: {current_conversation_id}**
+(Use this ID when calling schedule tools that require conversationId)
+
+**Available Schedule Tools:**
+- **createScheduleForConversation**: Create a new scheduled task (requires 
conversationId, skillName, cronExpression)
+- **listSchedulesForConversation**: List all scheduled tasks for a conversation
+- **deleteSchedule**: Delete a scheduled task by ID
+- **toggleSchedule**: Enable or disable a scheduled task
+
+**Natural Language Time to Cron Conversion:**
+When users describe times in natural language, convert them to 6-digit Spring 
Cron format:
+`seconds minutes hours dayOfMonth month dayOfWeek`
+
+Common conversions:
+- "daily at 9am" → `0 0 9 * * ?`
+- "daily at 6pm" → `0 0 18 * * ?`
+- "every Monday at 9am" → `0 0 9 ? * MON`
+- "weekdays at 8:30am" → `0 30 8 ? * MON-FRI`
+- "first day of each month" → `0 0 0 1 * ?`
+- "every hour" → `0 0 * * * ?`
+- "every 30 minutes" → `0 */30 * * * ?`
+
+**Workflow:**
+1. When user wants to schedule a skill, identify the skill name and time
+2. Convert natural language time to Cron expression
+3. **Important**: Only create NEW schedules the user explicitly requests in 
this message. Do NOT recreate schedules from conversation history.
+4. Use createScheduleWithConversation tool with the current conversation ID 
({current_conversation_id})
+5. Confirm creation with task details and next run time
+
 
 ## Natural Language Examples:
 
@@ -80,7 +112,12 @@ If no skill matches, use individual tools to assist the 
user.
 - "Find all alerts for monitor ID 1234 in the past day"
 - "Which monitors are currently abnormal?"
 
-
+### Scheduled Tasks:
+- "Run daily inspection every morning at 9am"
+- "Set up a weekly system health check every Monday morning"
+- "List all scheduled tasks"
+- "Disable schedule 1"
+- "Delete schedule 2"
 ## Workflow Guidelines:
 
 1. **Adding Monitors**:
diff --git a/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml 
b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
index eba0795104..2a5028ea6f 100644
--- a/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
+++ b/hertzbeat-ai/src/main/resources/skills/daily_inspection.yml
@@ -6,7 +6,7 @@ version: "1.0"
 output:
   type: report           # report / simple / data / action
   format: markdown       # markdown / json / text
-  language: zh           # zh (Chinese) / en (English) - controls LLM response 
language
+  # language is not specified here - uses system default locale from user's 
SystemConfig
   contentStep: generate_report  # Which step's output is the main content
 
 parameters:
diff --git 
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/SopSchedule.java
 
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/SopSchedule.java
new file mode 100644
index 0000000000..d0efbfb38f
--- /dev/null
+++ 
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/SopSchedule.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.ai;
+
+import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+/**
+ * Entity for storing scheduled SOP execution configurations.
+ * Allows users to schedule automatic SOP executions with results pushed to 
conversations.
+ */
+@Data
+@Builder
+@Entity
+@EntityListeners(AuditingEntityListener.class)
+@Table(name = "hzb_sop_schedule", indexes = {
+    @Index(name = "idx_schedule_conversation_id", columnList = 
"conversation_id"),
+    @Index(name = "idx_schedule_enabled_next", columnList = "enabled, 
next_run_time")
+})
+@AllArgsConstructor
+@NoArgsConstructor
+public class SopSchedule {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Schema(title = "Conversation ID to push results to")
+    @NotNull
+    @Column(name = "conversation_id")
+    private Long conversationId;
+
+    @Schema(title = "Name of the SOP skill to execute")
+    @NotBlank
+    @Column(name = "sop_name", length = 64)
+    private String sopName;
+
+    @Schema(title = "SOP execution parameters in JSON format")
+    @Column(name = "sop_params", length = 1024)
+    private String sopParams;
+
+    @Schema(title = "Cron expression for scheduling")
+    @NotBlank
+    @Column(name = "cron_expression", length = 64)
+    private String cronExpression;
+
+    @Schema(title = "Whether the schedule is enabled")
+    @Builder.Default
+    @Column(name = "enabled")
+    private Boolean enabled = true;
+
+    @Schema(title = "Last execution time")
+    @Column(name = "last_run_time")
+    private LocalDateTime lastRunTime;
+
+    @Schema(title = "Next scheduled execution time")
+    @Column(name = "next_run_time")
+    private LocalDateTime nextRunTime;
+
+    @Schema(title = "The creator of this record", accessMode = READ_ONLY)
+    @CreatedBy
+    private String creator;
+
+    @Schema(title = "The modifier of this record", accessMode = READ_ONLY)
+    @LastModifiedBy
+    private String modifier;
+
+    @Schema(title = "Record create time", accessMode = READ_ONLY)
+    @CreatedDate
+    private LocalDateTime gmtCreate;
+
+    @Schema(title = "Record modify time", accessMode = READ_ONLY)
+    @LastModifiedDate
+    private LocalDateTime gmtUpdate;
+}
diff --git 
a/hertzbeat-startup/src/main/resources/db/migration/h2/V181__update_column.sql 
b/hertzbeat-startup/src/main/resources/db/migration/h2/V181__update_column.sql
new file mode 100644
index 0000000000..6cc4c92b90
--- /dev/null
+++ 
b/hertzbeat-startup/src/main/resources/db/migration/h2/V181__update_column.sql
@@ -0,0 +1,18 @@
+-- Scheduled SOP execution configurations
+CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    conversation_id BIGINT NOT NULL COMMENT 'Conversation ID to push results 
to',
+    sop_name VARCHAR(64) NOT NULL COMMENT 'Name of the SOP skill to execute',
+    sop_params VARCHAR(1024) COMMENT 'SOP execution parameters in JSON format',
+    cron_expression VARCHAR(64) NOT NULL COMMENT 'Cron expression for 
scheduling',
+    enabled TINYINT DEFAULT 1 COMMENT 'Whether the schedule is enabled',
+    last_run_time DATETIME COMMENT 'Last execution time',
+    next_run_time DATETIME COMMENT 'Next scheduled execution time',
+    creator VARCHAR(64) COMMENT 'Creator of this record',
+    modifier VARCHAR(64) COMMENT 'Last modifier',
+    gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
+    gmt_update DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 
COMMENT 'Update time'
+);
+
+CREATE INDEX IF NOT EXISTS idx_schedule_conversation_id ON 
hzb_sop_schedule(conversation_id);
+CREATE INDEX IF NOT EXISTS idx_schedule_enabled_next ON 
hzb_sop_schedule(enabled, next_run_time);
diff --git 
a/hertzbeat-startup/src/main/resources/db/migration/mysql/V181__update_column.sql
 
b/hertzbeat-startup/src/main/resources/db/migration/mysql/V181__update_column.sql
new file mode 100644
index 0000000000..219efe0e95
--- /dev/null
+++ 
b/hertzbeat-startup/src/main/resources/db/migration/mysql/V181__update_column.sql
@@ -0,0 +1,17 @@
+-- Scheduled SOP execution configurations
+CREATE TABLE hzb_sop_schedule (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    conversation_id BIGINT NOT NULL COMMENT 'Conversation ID to push results 
to',
+    sop_name VARCHAR(64) NOT NULL COMMENT 'Name of the SOP skill to execute',
+    sop_params VARCHAR(1024) COMMENT 'SOP execution parameters in JSON format',
+    cron_expression VARCHAR(64) NOT NULL COMMENT 'Cron expression for 
scheduling',
+    enabled TINYINT DEFAULT 1 COMMENT 'Whether the schedule is enabled',
+    last_run_time DATETIME COMMENT 'Last execution time',
+    next_run_time DATETIME COMMENT 'Next scheduled execution time',
+    creator VARCHAR(64) COMMENT 'Creator of this record',
+    modifier VARCHAR(64) COMMENT 'Last modifier',
+    gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
+    gmt_update DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 
COMMENT 'Update time',
+    INDEX idx_schedule_conversation_id (conversation_id),
+    INDEX idx_schedule_enabled_next (enabled, next_run_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
diff --git 
a/hertzbeat-startup/src/main/resources/db/migration/postgresql/V181__update_column.sql
 
b/hertzbeat-startup/src/main/resources/db/migration/postgresql/V181__update_column.sql
new file mode 100644
index 0000000000..dac5d4db54
--- /dev/null
+++ 
b/hertzbeat-startup/src/main/resources/db/migration/postgresql/V181__update_column.sql
@@ -0,0 +1,27 @@
+-- Scheduled SOP execution configurations
+CREATE TABLE hzb_sop_schedule (
+    id BIGSERIAL PRIMARY KEY,
+    conversation_id BIGINT NOT NULL,
+    sop_name VARCHAR(64) NOT NULL,
+    sop_params VARCHAR(1024),
+    cron_expression VARCHAR(64) NOT NULL,
+    enabled SMALLINT DEFAULT 1,
+    last_run_time TIMESTAMP,
+    next_run_time TIMESTAMP,
+    creator VARCHAR(64),
+    modifier VARCHAR(64),
+    gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    gmt_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+COMMENT ON TABLE hzb_sop_schedule IS 'Scheduled SOP execution configurations';
+COMMENT ON COLUMN hzb_sop_schedule.conversation_id IS 'Conversation ID to push 
results to';
+COMMENT ON COLUMN hzb_sop_schedule.sop_name IS 'Name of the SOP skill to 
execute';
+COMMENT ON COLUMN hzb_sop_schedule.sop_params IS 'SOP execution parameters in 
JSON format';
+COMMENT ON COLUMN hzb_sop_schedule.cron_expression IS 'Cron expression for 
scheduling';
+COMMENT ON COLUMN hzb_sop_schedule.enabled IS 'Whether the schedule is 
enabled';
+COMMENT ON COLUMN hzb_sop_schedule.last_run_time IS 'Last execution time';
+COMMENT ON COLUMN hzb_sop_schedule.next_run_time IS 'Next scheduled execution 
time';
+
+CREATE INDEX idx_schedule_conversation_id ON hzb_sop_schedule(conversation_id);
+CREATE INDEX idx_schedule_enabled_next ON hzb_sop_schedule(enabled, 
next_run_time);
diff --git a/web-app/src/app/service/ai-chat.service.ts 
b/web-app/src/app/service/ai-chat.service.ts
index 96e54c0ae9..88bb95fc91 100644
--- a/web-app/src/app/service/ai-chat.service.ts
+++ b/web-app/src/app/service/ai-chat.service.ts
@@ -26,7 +26,7 @@ import { LocalStorageService } from './local-storage.service';
 
 export interface ChatMessage {
   content: string;
-  role: 'user' | 'assistant';
+  role: 'user' | 'assistant' | 'system_push';
   gmtCreate: Date;
   /** Flag indicating this message is a skill report that should be displayed 
directly */
   isSkillReport?: boolean;
@@ -45,7 +45,26 @@ export interface ChatRequestContext {
   conversationId?: number;
 }
 
+export interface SopSchedule {
+  id?: number;
+  conversationId: number;
+  sopName: string;
+  sopParams?: string;
+  cronExpression: string;
+  enabled: boolean;
+  lastRunTime?: Date;
+  nextRunTime?: Date;
+  gmtCreate?: Date;
+  gmtUpdate?: Date;
+}
+
+export interface SkillInfo {
+  name: string;
+  description: string;
+}
+
 const chat_uri = '/chat';
+const schedule_uri = '/ai/schedule';
 
 @Injectable({
   providedIn: 'root'
@@ -180,4 +199,48 @@ export class AiChatService {
 
     return responseSubject.asObservable();
   }
+
+  // ===== Schedule API Methods =====
+
+  /**
+   * Get all schedules for a conversation
+   */
+  getSchedules(conversationId: number): Observable<Message<SopSchedule[]>> {
+    return 
this.http.get<Message<SopSchedule[]>>(`${schedule_uri}/conversation/${conversationId}`);
+  }
+
+  /**
+   * Create a new schedule
+   */
+  createSchedule(schedule: SopSchedule): Observable<Message<SopSchedule>> {
+    return this.http.post<Message<SopSchedule>>(schedule_uri, schedule);
+  }
+
+  /**
+   * Update a schedule
+   */
+  updateSchedule(id: number, schedule: SopSchedule): 
Observable<Message<SopSchedule>> {
+    return this.http.put<Message<SopSchedule>>(`${schedule_uri}/${id}`, 
schedule);
+  }
+
+  /**
+   * Delete a schedule
+   */
+  deleteSchedule(id: number): Observable<Message<void>> {
+    return this.http.delete<Message<void>>(`${schedule_uri}/${id}`);
+  }
+
+  /**
+   * Toggle schedule enabled status
+   */
+  toggleSchedule(id: number, enabled: boolean): 
Observable<Message<SopSchedule>> {
+    return 
this.http.put<Message<SopSchedule>>(`${schedule_uri}/${id}/toggle?enabled=${enabled}`,
 {});
+  }
+
+  /**
+   * Get available SOP skills
+   */
+  getAvailableSkills(): Observable<Message<SkillInfo[]>> {
+    return this.http.get<Message<SkillInfo[]>>(`${schedule_uri}/skills`);
+  }
 }
diff --git a/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts 
b/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts
index a154954c7a..f2b061cc88 100644
--- a/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts
+++ b/web-app/src/app/shared/components/ai-chat/ai-chat.module.ts
@@ -23,6 +23,7 @@ import { FormsModule, ReactiveFormsModule } from 
'@angular/forms';
 import { DelonFormModule } from '@delon/form';
 import { AlainThemeModule } from '@delon/theme';
 import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzDividerModule } from 'ng-zorro-antd/divider';
 import { NzFormModule } from 'ng-zorro-antd/form';
 import { NzIconModule } from 'ng-zorro-antd/icon';
 import { NzInputModule } from 'ng-zorro-antd/input';
@@ -30,6 +31,8 @@ import { NzMessageModule } from 'ng-zorro-antd/message';
 import { NzModalModule } from 'ng-zorro-antd/modal';
 import { NzSelectModule } from 'ng-zorro-antd/select';
 import { NzSpinModule } from 'ng-zorro-antd/spin';
+import { NzSwitchModule } from 'ng-zorro-antd/switch';
+import { NzTableModule } from 'ng-zorro-antd/table';
 import { NzTooltipDirective } from 'ng-zorro-antd/tooltip';
 import { MarkdownComponent } from 'ngx-markdown';
 
@@ -44,6 +47,7 @@ import { ChatComponent } from './chat.component';
     AlainThemeModule.forChild(),
     DelonFormModule,
     NzButtonModule,
+    NzDividerModule,
     NzFormModule,
     NzIconModule,
     NzInputModule,
@@ -51,9 +55,11 @@ import { ChatComponent } from './chat.component';
     NzModalModule,
     NzSelectModule,
     NzSpinModule,
+    NzSwitchModule,
+    NzTableModule,
     MarkdownComponent,
     NzTooltipDirective
   ],
   exports: [ChatComponent]
 })
-export class AiChatModule {}
+export class AiChatModule { }
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.html 
b/web-app/src/app/shared/components/ai-chat/chat.component.html
index a09c0a91c1..08f84c6d51 100644
--- a/web-app/src/app/shared/components/ai-chat/chat.component.html
+++ b/web-app/src/app/shared/components/ai-chat/chat.component.html
@@ -29,12 +29,8 @@
     </div>
 
     <div class="conversation-list">
-      <div
-        *ngFor="let conversation of conversations"
-        class="conversation-item"
-        [class.active]="currentConversation?.id === conversation.id"
-        (click)="selectConversation(conversation)"
-      >
+      <div *ngFor="let conversation of conversations" class="conversation-item"
+        [class.active]="currentConversation?.id === conversation.id" 
(click)="selectConversation(conversation)">
         <div class="conversation-content">
           <div class="conversation-title">
             {{ getConversationTitle(conversation) }}
@@ -43,7 +39,8 @@
             {{ conversation.gmtUpdate | date : 'MMM d, HH:mm' }}
           </div>
         </div>
-        <button nz-button nzType="text" nzSize="small" nzDanger 
(click)="deleteConversation(conversation, $event)" class="delete-btn">
+        <button nz-button nzType="text" nzSize="small" nzDanger 
(click)="deleteConversation(conversation, $event)"
+          class="delete-btn">
           <i nz-icon nzType="delete"></i>
         </button>
       </div>
@@ -64,6 +61,17 @@
         </h2>
         <h2 *ngIf="!currentConversation">{{ 'ai.chat.title' | i18n }}</h2>
       </div>
+
+      <!-- Spacer to push actions to the left of page-level buttons -->
+      <div class="header-spacer"></div>
+
+      <div class="chat-header-actions" *ngIf="currentConversation">
+        <button nz-button nzType="primary" nzSize="small" nzGhost 
(click)="onShowScheduleModal()" nz-tooltip
+          [nzTooltipTitle]="'ai.chat.schedule.tooltip' | i18n">
+          <i nz-icon nzType="clock-circle" nzTheme="outline"></i>
+          <span>{{ 'ai.chat.schedule.button' | i18n }}</span>
+        </button>
+      </div>
     </div>
 
     <!-- Messages area -->
@@ -81,7 +89,8 @@
       <!-- Welcome message -->
       <div *ngIf="messages.length === 0 && !isLoadingConversations" 
class="welcome-message">
         <div class="welcome-content">
-          <img [src]="theme === 'dark' ? 'assets/logo_white.svg' : 
'assets/logo.svg'" alt="HertzBeat Logo" class="welcome-icon" />
+          <img [src]="theme === 'dark' ? 'assets/logo_white.svg' : 
'assets/logo.svg'" alt="HertzBeat Logo"
+            class="welcome-icon" />
           <h3 class="welcome-title">{{ 'ai.chat.welcome.title' | i18n }}</h3>
           <p class="welcome-description">
             {{ 'ai.chat.welcome.description' | i18n }}
@@ -98,27 +107,36 @@
       </div>
 
       <!-- Chat messages -->
-      <div *ngFor="let message of messages; let i = index" class="message" 
[class.user-message]="message.role === 'user'">
+      <div *ngFor="let message of messages; let i = index" class="message"
+        [class.user-message]="message.role === 'user'" 
[class.system-push-message]="message.role === 'system_push'">
         <div class="message-avatar">
-          <i nz-icon [nzType]="message.role === 'user' ? 'user' : 'robot'"></i>
+          <i nz-icon
+            [nzType]="message.role === 'user' ? 'user' : (message.role === 
'system_push' ? 'bell' : 'robot')"></i>
         </div>
         <div class="message-content">
           <div class="message-header">
-            <span class="message-role">{{ message.role === 'user' ? 
('ai.chat.user' | i18n) : ('ai.chat.assistant' | i18n) }}</span>
+            <span class="message-role">
+              <ng-container [ngSwitch]="message.role">
+                <span *ngSwitchCase="'user'">{{ 'ai.chat.user' | i18n }}</span>
+                <span *ngSwitchCase="'system_push'">🔔 定时推送</span>
+                <span *ngSwitchDefault>{{ 'ai.chat.assistant' | i18n }}</span>
+              </ng-container>
+            </span>
             <span class="message-time">{{ formatTime(message.gmtCreate) 
}}</span>
             <span
               *ngIf="message.role === 'assistant' && isSendingMessage && i === 
messages.length - 1 && message.content !== ''"
-              class="streaming-indicator"
-            >
+              class="streaming-indicator">
               <i nz-icon nzType="loading" nzSpin style="font-size: 12px"></i>
               <span style="font-size: 11px">Typing...</span>
             </span>
           </div>
           <div class="message-text">
             <div *ngIf="message.role === 'user'" 
[innerHTML]="message.content"></div>
-            <markdown *ngIf="message.role === 'assistant'" 
[data]="message.content"></markdown>
+            <markdown *ngIf="message.role === 'assistant' || message.role === 
'system_push'" [data]="message.content">
+            </markdown>
           </div>
-          <div *ngIf="message.role === 'assistant' && message.content === '' 
&& isSendingMessage" class="typing-indicator">
+          <div *ngIf="message.role === 'assistant' && message.content === '' 
&& isSendingMessage"
+            class="typing-indicator">
             <span>{{ 'ai.chat.typing' | i18n }}</span>
           </div>
         </div>
@@ -129,17 +147,11 @@
     <div class="input-area">
       <div class="input-container">
         <nz-textarea-count [nzMaxCharacterCount]="1000">
-          <textarea
-            nz-input
-            [(ngModel)]="newMessage"
-            (keypress)="onKeyPress($event)"
-            [disabled]="isSendingMessage"
-            placeholder="{{ 'ai.chat.input.placeholder' | i18n }}"
-            rows="3"
-            style="resize: none"
-          ></textarea>
+          <textarea nz-input [(ngModel)]="newMessage" 
(keypress)="onKeyPress($event)" [disabled]="isSendingMessage"
+            placeholder="{{ 'ai.chat.input.placeholder' | i18n }}" rows="3" 
style="resize: none"></textarea>
         </nz-textarea-count>
-        <button nz-button nzType="primary" (click)="sendMessage()" 
[disabled]="!newMessage.trim() || isSendingMessage" class="send-button">
+        <button nz-button nzType="primary" (click)="sendMessage()" 
[disabled]="!newMessage.trim() || isSendingMessage"
+          class="send-button">
           <i nz-icon [nzType]="isSendingMessage ? 'loading' : 'send'" 
[nzSpin]="isSendingMessage"></i>
         </button>
       </div>
@@ -155,32 +167,21 @@
 </div>
 
 <!-- OpenAI Configuration Modal -->
-<nz-modal
-  [(nzVisible)]="showConfigModal"
-  [nzTitle]="'ai.chat.config.title' | i18n"
-  (nzOnCancel)="onCloseConfigModal()"
-  (nzOnOk)="onSaveAiProviderConfig()"
-  nzMaskClosable="false"
-  [nzClosable]="false"
-  nzWidth="700px"
-  [nzOkLoading]="configLoading"
-  [nzOkText]="'ai.chat.config.save' | i18n"
-  [nzCancelText]="'ai.chat.config.cancel' | i18n"
->
+<nz-modal [(nzVisible)]="showConfigModal" [nzTitle]="'ai.chat.config.title' | 
i18n" (nzOnCancel)="onCloseConfigModal()"
+  (nzOnOk)="onSaveAiProviderConfig()" nzMaskClosable="false" 
[nzClosable]="false" nzWidth="700px"
+  [nzOkLoading]="configLoading" [nzOkText]="'ai.chat.config.save' | i18n"
+  [nzCancelText]="'ai.chat.config.cancel' | i18n">
   <div *nzModalContent class="-inner-content">
     <form nz-form nzLayout="vertical">
       <!-- Provider Selection -->
       <nz-form-item>
         <nz-form-label nzRequired="true">{{ 'ai.chat.config.provider' | i18n 
}}</nz-form-label>
         <nz-form-control [nzErrorTip]="'ai.chat.config.provider.required' | 
i18n">
-          <nz-select
-            [(ngModel)]="aiProviderConfig.code"
-            name="provider"
-            [nzPlaceHolder]="'ai.chat.config.provider.help' | i18n"
-            (ngModelChange)="onProviderChange($event)"
-            style="width: 100%"
-          >
-            <nz-option *ngFor="let option of providerOptions" 
[nzValue]="option.value" [nzLabel]="option.label"></nz-option>
+          <nz-select [(ngModel)]="aiProviderConfig.code" name="provider"
+            [nzPlaceHolder]="'ai.chat.config.provider.help' | i18n" 
(ngModelChange)="onProviderChange($event)"
+            style="width: 100%">
+            <nz-option *ngFor="let option of providerOptions" 
[nzValue]="option.value"
+              [nzLabel]="option.label"></nz-option>
           </nz-select>
           <p class="form-help">Choose your AI provider (OpenAI, ZhiPu, or 
ZAI)</p>
         </nz-form-control>
@@ -190,7 +191,8 @@
       <nz-form-item>
         <nz-form-label nzRequired="true">{{ 'ai.chat.config.api-key' | i18n 
}}</nz-form-label>
         <nz-form-control [nzErrorTip]="'ai.chat.config.api-key.required' | 
i18n">
-          <input nz-input [(ngModel)]="aiProviderConfig.apiKey" name="apiKey" 
type="password" placeholder="sk-..." required />
+          <input nz-input [(ngModel)]="aiProviderConfig.apiKey" name="apiKey" 
type="password" placeholder="sk-..."
+            required />
           <p class="form-help">{{ 'ai.chat.config.api-key.help' | i18n }}</p>
         </nz-form-control>
       </nz-form-item>
@@ -200,16 +202,11 @@
         <nz-form-label>{{ 'ai.chat.config.base-url' | i18n }}</nz-form-label>
         <nz-form-control>
           <nz-input-group [nzSuffix]="resetBtn">
-            <input nz-input [(ngModel)]="aiProviderConfig.baseUrl" 
name="baseUrl" placeholder="https://api.openai.com/v1"; />
+            <input nz-input [(ngModel)]="aiProviderConfig.baseUrl" 
name="baseUrl"
+              placeholder="https://api.openai.com/v1"; />
             <ng-template #resetBtn>
-              <button
-                nz-button
-                nzType="link"
-                nzSize="small"
-                (click)="resetToDefaults()"
-                [nzTooltipTitle]="'ai.chat.config.reset' | i18n"
-                nz-tooltip
-              >
+              <button nz-button nzType="link" nzSize="small" 
(click)="resetToDefaults()"
+                [nzTooltipTitle]="'ai.chat.config.reset' | i18n" nz-tooltip>
                 <i nz-icon nzType="reload"></i>
               </button>
             </ng-template>
@@ -229,3 +226,117 @@
     </form>
   </div>
 </nz-modal>
+
+<!-- Schedule Configuration Modal -->
+<nz-modal [(nzVisible)]="showScheduleModal" 
[nzTitle]="'ai.chat.schedule.title' | i18n"
+  (nzOnCancel)="onCloseScheduleModal()" [nzFooter]="null" nzWidth="600px">
+  <div *nzModalContent class="schedule-modal-content">
+    <!-- Existing Schedules List -->
+    <div class="schedule-list" *ngIf="schedules.length > 0">
+      <h4>{{ 'ai.chat.schedule.configured' | i18n }}</h4>
+      <nz-table #scheduleTable [nzData]="schedules" [nzShowPagination]="false" 
nzSize="small">
+        <thead>
+          <tr>
+            <th>{{ 'ai.chat.schedule.skill' | i18n }}</th>
+            <th>{{ 'ai.chat.schedule.cron' | i18n }}</th>
+            <th>{{ 'ai.chat.schedule.status' | i18n }}</th>
+            <th>{{ 'ai.chat.schedule.action' | i18n }}</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr *ngFor="let schedule of scheduleTable.data">
+            <td>{{ schedule.sopName }}</td>
+            <td>{{ schedule.cronExpression }}</td>
+            <td>
+              <nz-switch [(ngModel)]="schedule.enabled" 
(ngModelChange)="onToggleSchedule(schedule)"></nz-switch>
+            </td>
+            <td>
+              <button nz-button nzType="text" nzSize="small" 
(click)="onEditSchedule(schedule)" nz-tooltip
+                [nzTooltipTitle]="'common.button.edit' | i18n">
+                <i nz-icon nzType="edit"></i>
+              </button>
+              <button nz-button nzType="text" nzDanger nzSize="small" 
(click)="onDeleteSchedule(schedule)" nz-tooltip
+                [nzTooltipTitle]="'common.button.delete' | i18n">
+                <i nz-icon nzType="delete"></i>
+              </button>
+            </td>
+          </tr>
+        </tbody>
+      </nz-table>
+    </div>
+
+    <nz-divider *ngIf="schedules.length > 0"></nz-divider>
+
+    <!-- Edit Schedule Form -->
+    <div *ngIf="editingSchedule">
+      <h4>{{ 'ai.chat.schedule.edit' | i18n }}</h4>
+      <form nz-form nzLayout="vertical">
+        <nz-form-item>
+          <nz-form-label>{{ 'ai.chat.schedule.skill' | i18n }}</nz-form-label>
+          <nz-form-control>
+            <input nz-input [value]="editingSchedule.sopName" disabled />
+          </nz-form-control>
+        </nz-form-item>
+
+        <nz-form-item>
+          <nz-form-label nzRequired>{{ 'ai.chat.schedule.cron.label' | i18n 
}}</nz-form-label>
+          <nz-form-control>
+            <input nz-input [(ngModel)]="editingSchedule.cronExpression" 
name="editCronExpression"
+              placeholder="0 0 9 * * ?" />
+          </nz-form-control>
+        </nz-form-item>
+
+        <nz-form-item>
+          <button nz-button nzType="primary" (click)="onUpdateSchedule()" 
[nzLoading]="scheduleLoading"
+            [disabled]="!editingSchedule.cronExpression" style="margin-right: 
8px">
+            <i nz-icon nzType="save"></i> {{ 'common.button.save' | i18n }}
+          </button>
+          <button nz-button (click)="onCancelEdit()">
+            {{ 'common.button.cancel' | i18n }}
+          </button>
+        </nz-form-item>
+      </form>
+      <nz-divider></nz-divider>
+    </div>
+
+    <!-- Add New Schedule Form -->
+    <h4>{{ schedules.length > 0 ? ('ai.chat.schedule.add' | i18n) : 
('ai.chat.schedule.create' | i18n) }}</h4>
+    <form nz-form nzLayout="vertical">
+      <nz-form-item>
+        <nz-form-label nzRequired>{{ 'ai.chat.schedule.skill.select' | i18n 
}}</nz-form-label>
+        <nz-form-control>
+          <nz-select [(ngModel)]="newSchedule.sopName" name="sopName"
+            [nzPlaceHolder]="'ai.chat.schedule.skill.placeholder' | i18n" 
style="width: 100%">
+            <nz-option *ngFor="let skill of availableSkills" 
[nzValue]="skill.name"
+              [nzLabel]="skill.name + ' - ' + skill.description">
+            </nz-option>
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+
+      <nz-form-item>
+        <nz-form-label nzRequired>{{ 'ai.chat.schedule.cron.label' | i18n 
}}</nz-form-label>
+        <nz-form-control>
+          <nz-input-group [nzSuffix]="cronHelpTpl">
+            <input nz-input [(ngModel)]="newSchedule.cronExpression" 
name="cronExpression" placeholder="0 0 9 * * ?" />
+          </nz-input-group>
+          <ng-template #cronHelpTpl>
+            <i nz-icon nzType="question-circle" nz-tooltip
+              [nzTooltipTitle]="('ai.chat.schedule.cron.help' | i18n) + 
'&#10;' + ('ai.chat.schedule.cron.example.daily' | i18n) + '&#10;' + 
('ai.chat.schedule.cron.example.weekly' | i18n)"></i>
+          </ng-template>
+          <p class="form-help">
+            {{ 'ai.chat.schedule.cron.common' | i18n }} <code>0 0 9 * * 
?</code> | {{ 'ai.chat.schedule.cron.monday' |
+            i18n }} <code>0 0 9 ? * MON</code>
+          </p>
+        </nz-form-control>
+      </nz-form-item>
+
+      <nz-form-item>
+        <button nz-button nzType="primary" (click)="onCreateSchedule()" 
[nzLoading]="scheduleLoading"
+          [disabled]="!newSchedule.sopName || !newSchedule.cronExpression">
+          <i nz-icon nzType="plus"></i> {{ 'ai.chat.schedule.create' | i18n }}
+        </button>
+      </nz-form-item>
+    </form>
+  </div>
+</nz-modal>
\ No newline at end of file
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.less 
b/web-app/src/app/shared/components/ai-chat/chat.component.less
index 23a198c5d4..48427a11da 100644
--- a/web-app/src/app/shared/components/ai-chat/chat.component.less
+++ b/web-app/src/app/shared/components/ai-chat/chat.component.less
@@ -143,10 +143,32 @@
         font-size: 16px;
       }
 
-      .chat-title h2 {
-        margin: 0;
-        font-size: 18px;
-        color: @heading-color;
+      .chat-title {
+        flex: 1;
+
+        h2 {
+          margin: 0;
+          font-size: 18px;
+          color: @heading-color;
+        }
+      }
+
+      .header-spacer {
+        flex: 1;
+      }
+
+      .chat-header-actions {
+        margin-right: 80px; // Leave space for page-level buttons
+
+        button {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+
+          i {
+            font-size: 14px;
+          }
+        }
       }
     }
 
@@ -267,6 +289,26 @@
           }
         }
 
+        &.system-push-message {
+          .message-avatar {
+            background: #e6f7ff;
+
+            i {
+              color: #1890ff;
+            }
+          }
+
+          .message-content {
+            background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
+            border-left: 3px solid #1890ff;
+
+            .message-header .message-role {
+              color: #096dd9;
+              font-weight: 600;
+            }
+          }
+        }
+
         .message-avatar {
           width: 32px;
           height: 32px;
@@ -381,6 +423,7 @@
     opacity: 0;
     transform: translateY(10px);
   }
+
   to {
     opacity: 1;
     transform: translateY(0);
@@ -406,6 +449,7 @@
     .chat-main {
       .messages-container {
         .message {
+
           &.user-message .message-content,
           .message-content {
             max-width: 85%;
@@ -475,140 +519,141 @@ body[data-theme='dark'] .chat-container {
       color: fade(@white, 85%);
     }
 
-      .conversation-list {
-        .conversation-item {
-          border-bottom-color: @border-color-split;
-          background: transparent;
-          border-left: 3px solid transparent;
+    .conversation-list {
+      .conversation-item {
+        border-bottom-color: @border-color-split;
+        background: transparent;
+        border-left: 3px solid transparent;
 
-          &:hover {
-            background: transparent;
+        &:hover {
+          background: transparent;
 
-            .conversation-content .conversation-title {
-              color: @primary-color;
-            }
+          .conversation-content .conversation-title {
+            color: @primary-color;
           }
+        }
 
-          &.active {
-            background: fade(@white, 5%);
-            border-left: 3px solid @primary-color;
+        &.active {
+          background: fade(@white, 5%);
+          border-left: 3px solid @primary-color;
 
-            .conversation-content .conversation-title {
-              color: @primary-color;
-            }
+          .conversation-content .conversation-title {
+            color: @primary-color;
           }
+        }
 
-          .conversation-content {
-            .conversation-title {
-              color: fade(@white, 65%);
-            }
+        .conversation-content {
+          .conversation-title {
+            color: fade(@white, 65%);
+          }
 
-            .conversation-time {
-              color: fade(@white, 45%);
-            }
+          .conversation-time {
+            color: fade(@white, 45%);
           }
         }
       }
     }
+  }
 
-    .chat-main {
+  .chat-main {
+    background: #000;
+
+    .chat-header {
+      border-bottom-color: @border-color-base;
       background: #000;
 
-      .chat-header {
-        border-bottom-color: @border-color-base;
-        background: #000;
+      .chat-title h2 {
+        color: fade(@white, 85%);
+      }
+    }
+
+    .messages-container {
+      background: #000;
 
-        .chat-title h2 {
-          color: fade(@white, 85%);
+      .loading-container {
+        p {
+          color: fade(@white, 65%);
         }
       }
 
-      .messages-container {
-        background: #000;
+      .welcome-message .welcome-content {
+        h3 {
+          color: fade(@white, 85%);
+        }
 
-        .loading-container {
-          p {
-            color: fade(@white, 65%);
-          }
+        p,
+        ul {
+          color: fade(@white, 65%);
         }
+      }
 
-        .welcome-message .welcome-content {
-          h3 {
-            color: fade(@white, 85%);
-          }
+      .message {
+        .message-avatar {
+          background: fade(@white, 8%);
+          box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
 
-          p, ul {
+          i {
             color: fade(@white, 65%);
           }
         }
 
-        .message {
-          .message-avatar {
-            background: fade(@white, 8%);
-            box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+        .message-content {
+          background: #141414 !important;
+          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
 
-            i {
-              color: fade(@white, 65%);
+          .message-header {
+            .message-role {
+              color: fade(@white, 85%);
             }
-          }
 
-          .message-content {
-            background: #141414 !important;
-            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
-
-            .message-header {
-              .message-role {
-                color: fade(@white, 85%);
-              }
-
-              .message-time {
-                color: fade(@white, 45%);
-              }
-
-              .streaming-indicator {
-                color: @primary-color;
-              }
+            .message-time {
+              color: fade(@white, 45%);
             }
 
-            .message-text {
-              color: fade(@white, 85%);
+            .streaming-indicator {
+              color: @primary-color;
             }
+          }
 
-            .typing-indicator {
-              color: fade(@white, 45%);
-            }
+          .message-text {
+            color: fade(@white, 85%);
           }
 
-          &.user-message .message-content {
-            background: @primary-color;
+          .typing-indicator {
+            color: fade(@white, 45%);
+          }
+        }
 
-            .message-text {
-              color: @white;
-            }
+        &.user-message .message-content {
+          background: @primary-color;
 
-            .message-header {
-              .message-role {
-                color: fade(@white, 90%);
-              }
+          .message-text {
+            color: @white;
+          }
 
-              .message-time {
-                color: fade(@white, 70%);
-              }
+          .message-header {
+            .message-role {
+              color: fade(@white, 90%);
+            }
+
+            .message-time {
+              color: fade(@white, 70%);
             }
           }
         }
       }
+    }
 
-      .input-area {
-        background: #000 !important;
-        border-top-color: @border-color-base;
+    .input-area {
+      background: #000 !important;
+      border-top-color: @border-color-base;
 
-        .input-hint small {
-          color: fade(@white, 45%);
-        }
+      .input-hint small {
+        color: fade(@white, 45%);
       }
     }
   }
+}
 
 
 
@@ -624,7 +669,8 @@ body[data-theme='dark'] .chat-container {
         color: fade(@white, 45%) !important;
       }
 
-      &:focus, &:hover {
+      &:focus,
+      &:hover {
         border-color: @primary-color !important;
         box-shadow: 0 0 0 2px fade(@primary-color, 20%) !important;
       }
@@ -745,7 +791,7 @@ body[data-theme='dark'] .chat-container {
   .form-item-with-help {
     margin-bottom: 8px;
 
-    + .form-help {
+    +.form-help {
       margin-bottom: 16px;
     }
   }
@@ -763,4 +809,4 @@ nz-modal {
   .ant-form-item-label {
     padding-bottom: 4px;
   }
-}
+}
\ No newline at end of file
diff --git a/web-app/src/app/shared/components/ai-chat/chat.component.ts 
b/web-app/src/app/shared/components/ai-chat/chat.component.ts
index 482cb13399..26fa2da2d4 100644
--- a/web-app/src/app/shared/components/ai-chat/chat.component.ts
+++ b/web-app/src/app/shared/components/ai-chat/chat.component.ts
@@ -24,7 +24,7 @@ import { NzMessageService } from 'ng-zorro-antd/message';
 import { NzModalService } from 'ng-zorro-antd/modal';
 
 import { ModelProviderConfig, PROVIDER_OPTIONS, ProviderOption } from 
'../../../pojo/ModelProviderConfig';
-import { AiChatService, ChatMessage, ChatConversation } from 
'../../../service/ai-chat.service';
+import { AiChatService, ChatMessage, ChatConversation, SopSchedule, SkillInfo 
} from '../../../service/ai-chat.service';
 import { GeneralConfigService } from '../../../service/general-config.service';
 import { ThemeService } from '../../../service/theme.service';
 
@@ -61,6 +61,18 @@ export class ChatComponent implements OnInit, OnDestroy {
   aiProviderConfig: ModelProviderConfig = new ModelProviderConfig();
   providerOptions: ProviderOption[] = PROVIDER_OPTIONS;
 
+  // Schedule Configuration
+  showScheduleModal = false;
+  scheduleLoading = false;
+  schedules: SopSchedule[] = [];
+  availableSkills: SkillInfo[] = [];
+  newSchedule: Partial<SopSchedule> = {
+    sopName: '',
+    cronExpression: '',
+    enabled: true
+  };
+  editingSchedule: SopSchedule | null = null;
+
   constructor(
     private aiChatService: AiChatService,
     private message: NzMessageService,
@@ -649,4 +661,194 @@ export class ChatComponent implements OnInit, OnDestroy {
       this.aiProviderConfig.model = selectedProvider.defaultModel;
     }
   }
+
+  // ===== Schedule Management Methods =====
+
+  /**
+   * Show schedule configuration modal
+   */
+  onShowScheduleModal(): void {
+    if (!this.currentConversation) {
+      this.message.warning('请先选择或创建一个对话');
+      return;
+    }
+    this.showScheduleModal = true;
+    this.loadSchedules();
+    this.loadAvailableSkills();
+  }
+
+  /**
+   * Close schedule modal
+   */
+  onCloseScheduleModal(): void {
+    this.showScheduleModal = false;
+  }
+
+  /**
+   * Load available SOP skills from backend
+   */
+  loadAvailableSkills(): void {
+    this.aiChatService.getAvailableSkills().subscribe({
+      next: response => {
+        if (response.code === 0 && response.data) {
+          this.availableSkills = response.data;
+        }
+      },
+      error: error => {
+        console.error('Error loading skills:', error);
+        // Fallback to empty - user will see nothing in dropdown
+      }
+    });
+  }
+
+  /**
+   * Load schedules for current conversation
+   */
+  loadSchedules(): void {
+    if (!this.currentConversation) return;
+
+    this.aiChatService.getSchedules(this.currentConversation.id).subscribe({
+      next: response => {
+        if (response.code === 0 && response.data) {
+          this.schedules = response.data;
+        }
+      },
+      error: error => {
+        console.error('Error loading schedules:', error);
+        this.message.error('加载定时任务失败');
+      }
+    });
+  }
+
+  /**
+   * Create a new schedule
+   */
+  onCreateSchedule(): void {
+    if (!this.currentConversation || !this.newSchedule.sopName || 
!this.newSchedule.cronExpression) {
+      return;
+    }
+
+    this.scheduleLoading = true;
+    const schedule: SopSchedule = {
+      conversationId: this.currentConversation.id,
+      sopName: this.newSchedule.sopName,
+      cronExpression: this.newSchedule.cronExpression,
+      enabled: true
+    };
+
+    this.aiChatService.createSchedule(schedule).subscribe({
+      next: response => {
+        this.scheduleLoading = false;
+        if (response.code === 0 && response.data) {
+          this.schedules.push(response.data);
+          this.newSchedule = { sopName: '', cronExpression: '', enabled: true 
};
+          
this.message.success(this.i18nSvc.fanyi('ai.chat.schedule.create.success'));
+        } else {
+          
this.message.error(`${this.i18nSvc.fanyi('ai.chat.schedule.create.failed')}: 
${response.msg}`);
+        }
+      },
+      error: error => {
+        this.scheduleLoading = false;
+        console.error('Error creating schedule:', error);
+        
this.message.error(this.i18nSvc.fanyi('ai.chat.schedule.create.failed'));
+      }
+    });
+  }
+
+  /**
+   * Toggle schedule enabled status
+   */
+  onToggleSchedule(schedule: SopSchedule): void {
+    if (!schedule.id) return;
+
+    this.aiChatService.toggleSchedule(schedule.id, 
schedule.enabled).subscribe({
+      next: response => {
+        if (response.code === 0) {
+          
this.message.success(this.i18nSvc.fanyi('ai.chat.schedule.toggle.success'));
+        } else {
+          schedule.enabled = !schedule.enabled; // Revert on error
+          
this.message.error(`${this.i18nSvc.fanyi('ai.chat.schedule.toggle.failed')}: 
${response.msg}`);
+        }
+      },
+      error: error => {
+        schedule.enabled = !schedule.enabled; // Revert on error
+        console.error('Error toggling schedule:', error);
+        
this.message.error(this.i18nSvc.fanyi('ai.chat.schedule.toggle.failed'));
+      }
+    });
+  }
+
+  /**
+   * Delete a schedule
+   */
+  onDeleteSchedule(schedule: SopSchedule): void {
+    if (!schedule.id) return;
+
+    this.modal.confirm({
+      nzTitle: this.i18nSvc.fanyi('ai.chat.conversation.delete.title'),
+      nzContent: `${this.i18nSvc.fanyi('ai.chat.conversation.delete.content')} 
"${schedule.sopName}"`,
+      nzOkText: this.i18nSvc.fanyi('ai.chat.conversation.delete.confirm'),
+      nzOkDanger: true,
+      nzOnOk: () => {
+        this.aiChatService.deleteSchedule(schedule.id!).subscribe({
+          next: response => {
+            if (response.code === 0) {
+              this.schedules = this.schedules.filter(s => s.id !== 
schedule.id);
+              
this.message.success(this.i18nSvc.fanyi('ai.chat.schedule.delete.success'));
+            } else {
+              
this.message.error(`${this.i18nSvc.fanyi('ai.chat.schedule.delete.failed')}: 
${response.msg}`);
+            }
+          },
+          error: error => {
+            console.error('Error deleting schedule:', error);
+            
this.message.error(this.i18nSvc.fanyi('ai.chat.schedule.delete.failed'));
+          }
+        });
+      }
+    });
+  }
+
+  /**
+   * Start editing a schedule
+   */
+  onEditSchedule(schedule: SopSchedule): void {
+    this.editingSchedule = { ...schedule };
+  }
+
+  /**
+   * Cancel editing
+   */
+  onCancelEdit(): void {
+    this.editingSchedule = null;
+  }
+
+  /**
+   * Update an existing schedule
+   */
+  onUpdateSchedule(): void {
+    if (!this.editingSchedule || !this.editingSchedule.id) return;
+
+    this.scheduleLoading = true;
+    this.aiChatService.updateSchedule(this.editingSchedule.id, 
this.editingSchedule).subscribe({
+      next: response => {
+        this.scheduleLoading = false;
+        if (response.code === 0 && response.data) {
+          // Update the schedule in the list
+          const index = this.schedules.findIndex(s => s.id === 
this.editingSchedule!.id);
+          if (index !== -1) {
+            this.schedules[index] = response.data;
+          }
+          this.editingSchedule = null;
+          
this.message.success(this.i18nSvc.fanyi('ai.chat.schedule.update.success'));
+        } else {
+          
this.message.error(`${this.i18nSvc.fanyi('ai.chat.schedule.update.failed')}: 
${response.msg}`);
+        }
+      },
+      error: error => {
+        this.scheduleLoading = false;
+        console.error('Error updating schedule:', error);
+        
this.message.error(this.i18nSvc.fanyi('ai.chat.schedule.update.failed'));
+      }
+    });
+  }
 }
diff --git a/web-app/src/assets/i18n/en-US.json 
b/web-app/src/assets/i18n/en-US.json
index 013c6c9b72..0fc3b86584 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -499,6 +499,7 @@
   "common.button.ok": "OK",
   "common.button.return": "Return",
   "common.button.setting": "Setting",
+  "common.button.save": "Save",
   "common.confirm.cancel": "Please confirm whether to cancel monitor!",
   "common.confirm.cancel-batch": "Please confirm whether to cancel monitor in 
batches!",
   "common.confirm.clear-cache": "Please confirm whether to clear cache!",
@@ -695,8 +696,8 @@
   "log.stream.severity-number-placeholder": "Enter Severity Number",
   "log.stream.severity-text": "Severity:",
   "log.stream.severity-text-placeholder": "Enter Severity",
-  "log.stream.content" : "Log Content",
-  "log.stream.content-placeholder" : "Enter Log Content",
+  "log.stream.content": "Log Content",
+  "log.stream.content-placeholder": "Enter Log Content",
   "log.stream.show-filters": "Show Filters",
   "log.stream.span": "Span:",
   "log.stream.span-id": "Span ID:",
@@ -1149,5 +1150,34 @@
   "ai.chat.error.chat.response": "Failed to get AI response:",
   "ai.chat.error.processing": "Sorry, there was an error processing your 
request. Please check if the AI Agent service is running and try again.",
   "ai.chat.offline.mode": "AI Chat service unavailable. Running in offline 
mode.",
-  "ai.chat.offline.response": "I apologize, but the AI Chat service is 
currently unavailable. Please ensure the HertzBeat AI module is running and try 
again later."
-}
+  "ai.chat.offline.response": "I apologize, but the AI Chat service is 
currently unavailable. Please ensure the HertzBeat AI module is running and try 
again later.",
+  "ai.chat.schedule.title": "Schedule Configuration",
+  "ai.chat.schedule.create": "Create Schedule",
+  "ai.chat.schedule.add": "Add New Task",
+  "ai.chat.schedule.configured": "Configured Schedules",
+  "ai.chat.schedule.skill": "Skill",
+  "ai.chat.schedule.skill.select": "Select Skill",
+  "ai.chat.schedule.skill.placeholder": "Select skill to execute",
+  "ai.chat.schedule.cron": "Execution Time",
+  "ai.chat.schedule.cron.label": "Execution Time (Cron Expression)",
+  "ai.chat.schedule.cron.help": "Format: seconds minutes hours day month week",
+  "ai.chat.schedule.cron.example.daily": "Example: 0 0 9 * * ? (daily at 9am)",
+  "ai.chat.schedule.cron.example.weekly": "Example: 0 0 9 ? * MON (Monday 
9am)",
+  "ai.chat.schedule.cron.common": "Common: daily 9am",
+  "ai.chat.schedule.cron.monday": "Monday 9am",
+  "ai.chat.schedule.status": "Status",
+  "ai.chat.schedule.action": "Action",
+  "ai.chat.schedule.create.success": "Schedule created successfully",
+  "ai.chat.schedule.create.failed": "Failed to create schedule",
+  "ai.chat.schedule.delete.success": "Schedule deleted",
+  "ai.chat.schedule.delete.failed": "Failed to delete schedule",
+  "ai.chat.schedule.toggle.success": "Schedule status updated",
+  "ai.chat.schedule.toggle.failed": "Failed to toggle schedule",
+  "ai.chat.schedule.push": "Scheduled Push",
+  "ai.chat.schedule.button": "Schedule",
+  "ai.chat.schedule.tooltip": "Configure scheduled push tasks",
+  "ai.chat.schedule.edit": "Edit Schedule",
+  "ai.chat.schedule.skill": "Skill",
+  "ai.chat.schedule.update.success": "Schedule updated successfully",
+  "ai.chat.schedule.update.failed": "Failed to update schedule"
+}
\ No newline at end of file
diff --git a/web-app/src/assets/i18n/zh-CN.json 
b/web-app/src/assets/i18n/zh-CN.json
index d6ed897a01..320a1e0839 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -502,6 +502,7 @@
   "common.button.ok": "确定",
   "common.button.return": "返回",
   "common.button.setting": "配置",
+  "common.button.save": "保存",
   "common.confirm.cancel": "请确认是否取消!",
   "common.confirm.cancel-batch": "请确认是否批量取消!",
   "common.confirm.clear-cache": "请确认是否清理缓存!",
@@ -698,8 +699,8 @@
   "log.stream.severity-number-placeholder": "输入日志级别编号",
   "log.stream.severity-text": "日志级别:",
   "log.stream.severity-text-placeholder": "输入日志级别",
-  "log.stream.content" : "日志内容",
-  "log.stream.content-placeholder" : "输入日志内容",
+  "log.stream.content": "日志内容",
+  "log.stream.content-placeholder": "输入日志内容",
   "log.stream.show-filters": "显示过滤器",
   "log.stream.span": "跨度:",
   "log.stream.span-id": "Span ID:",
@@ -1152,5 +1153,34 @@
   "ai.chat.error.chat.response": "获取 AI 回复失败:",
   "ai.chat.error.processing": "处理您的请求时出错。请检查 AI 智能体服务是否正在运行,然后重试。",
   "ai.chat.offline.mode": "AI 聊天服务不可用。正在离线模式下运行。",
-  "ai.chat.offline.response": "抱歉,AI 聊天服务当前不可用。请确保 HertzBeat AI 模块正在运行,稍后再试。"
-}
+  "ai.chat.offline.response": "抱歉,AI 聊天服务当前不可用。请确保 HertzBeat AI 模块正在运行,稍后再试。",
+  "ai.chat.schedule.title": "定时任务设置",
+  "ai.chat.schedule.create": "创建定时任务",
+  "ai.chat.schedule.add": "添加新任务",
+  "ai.chat.schedule.configured": "已配置的定时任务",
+  "ai.chat.schedule.skill": "技能",
+  "ai.chat.schedule.skill.select": "选择技能",
+  "ai.chat.schedule.skill.placeholder": "选择要执行的技能",
+  "ai.chat.schedule.cron": "执行时间",
+  "ai.chat.schedule.cron.label": "执行时间 (Cron表达式)",
+  "ai.chat.schedule.cron.help": "格式: 秒 分 时 日 月 周",
+  "ai.chat.schedule.cron.example.daily": "示例: 0 0 9 * * ? (每天9点)",
+  "ai.chat.schedule.cron.example.weekly": "示例: 0 0 9 ? * MON (每周一9点)",
+  "ai.chat.schedule.cron.common": "常用: 每天9点",
+  "ai.chat.schedule.cron.monday": "每周一9点",
+  "ai.chat.schedule.status": "状态",
+  "ai.chat.schedule.action": "操作",
+  "ai.chat.schedule.create.success": "定时任务创建成功",
+  "ai.chat.schedule.create.failed": "创建定时任务失败",
+  "ai.chat.schedule.delete.success": "定时任务已删除",
+  "ai.chat.schedule.delete.failed": "删除定时任务失败",
+  "ai.chat.schedule.toggle.success": "定时任务状态已更新",
+  "ai.chat.schedule.toggle.failed": "更新定时任务状态失败",
+  "ai.chat.schedule.push": "定时推送",
+  "ai.chat.schedule.button": "定时任务",
+  "ai.chat.schedule.tooltip": "配置定时推送任务",
+  "ai.chat.schedule.edit": "编辑定时任务",
+  "ai.chat.schedule.skill": "技能",
+  "ai.chat.schedule.update.success": "定时任务更新成功",
+  "ai.chat.schedule.update.failed": "更新定时任务失败"
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to