diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java index 71c5d47cb58..2496c7ae621 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/controller/ChatController.java @@ -26,6 +26,7 @@ import org.apache.hertzbeat.ai.config.McpContextHolder; import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; import org.apache.hertzbeat.ai.service.ConversationService; import org.apache.hertzbeat.common.entity.ai.ChatConversation; import org.apache.hertzbeat.common.entity.dto.Message; @@ -78,12 +79,12 @@ public Flux> streamChat(@Valid @RequestBody C McpContextHolder.setSubject(subject); if (context.getMessage() == null || context.getMessage().trim().isEmpty()) { ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(context.getConversationId()) - .response("Error: Message cannot be empty") - .build(); + .conversationId(context.getConversationId()) + .response("Error: Message cannot be empty") + .build(); return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); + .event("error") + .build()); } log.info("Received streaming chat request for conversation: {}", context.getConversationId()); @@ -92,12 +93,12 @@ public Flux> streamChat(@Valid @RequestBody C } catch (Exception e) { log.error("Error in stream chat endpoint: ", e); ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(context.getConversationId()) - .response("An error occurred: " + e.getMessage()) - .build(); + .conversationId(context.getConversationId()) + .response("An error occurred: " + e.getMessage()) + .build(); return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); + .event("error") + .build()); } } @@ -134,7 +135,7 @@ public ResponseEntity>> listConversations() { @GetMapping(path = "/conversations/{conversationId}") @Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation") public ResponseEntity> getConversation( - @Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) { + @Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) { ChatConversation conversation = conversationService.getConversation(conversationId); return ResponseEntity.ok(Message.success(conversation)); } @@ -148,8 +149,21 @@ public ResponseEntity> getConversation( @DeleteMapping(path = "/conversations/{conversationId}") @Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages") public ResponseEntity> deleteConversation( - @Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) { + @Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) { conversationService.deleteConversation(conversationId); return ResponseEntity.ok(Message.success()); } + + /** + * Save data submitted by secure form + * @param securityData security data + * @return save result + */ + @PostMapping(path = "/security") + @Operation(summary = "save security data", description = "Save security data") + public ResponseEntity> commitSecurityData(@Valid @RequestBody SecurityData securityData) { + return ResponseEntity.ok(Message.success(conversationService.saveSecurityData(securityData))); + } + + } diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java new file mode 100644 index 00000000000..fb01d1ea935 --- /dev/null +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/pojo/dto/SecurityData.java @@ -0,0 +1,38 @@ +/* + * 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.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * security data + */ +@Data +public class SecurityData { + + @NotNull + @Schema(description = "Conversation ID", example = "123") + private Long conversationId; + + @NotNull + @Schema(description = "security data", example = "{\"password\":\"xxxxx\"}") + private String securityData; + +} diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java index a9eadef0520..8a007cf0a0f 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/ConversationService.java @@ -19,6 +19,7 @@ package org.apache.hertzbeat.ai.service; import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; import org.apache.hertzbeat.common.entity.ai.ChatConversation; import org.springframework.http.codec.ServerSentEvent; import reactor.core.publisher.Flux; @@ -33,7 +34,7 @@ public interface ConversationService { /** * Send a message and receive a streaming response * - * @param message The user's message + * @param message The user's message * @param conversationId Optional conversation ID for continuing a chat * @return Flux of ServerSentEvent for streaming the response */ @@ -67,4 +68,13 @@ public interface ConversationService { * @param conversationId Conversation ID to delete */ void deleteConversation(Long conversationId); + + /** + * save security data for a conversation + * + * @param securityData securityData + * @return save result + */ + Boolean saveSecurityData(SecurityData securityData); + } 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 b073e0f12e0..d4012076d84 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 @@ -18,15 +18,22 @@ package org.apache.hertzbeat.ai.service.impl; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.hertzbeat.common.entity.ai.ChatMessage; import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig; import org.apache.hertzbeat.ai.service.ChatClientProviderService; import org.apache.hertzbeat.base.dao.GeneralConfigDao; import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent; import org.apache.hertzbeat.common.util.JsonUtil; import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; @@ -43,9 +50,8 @@ import java.util.List; /** - * Implementation of the {@link ChatClientProviderService}. - * Provides functionality to interact with the ChatClient for handling chat - * messages. + * Implementation of the {@link ChatClientProviderService}. Provides functionality to interact with the ChatClient for + * handling chat messages. */ @Slf4j @Service @@ -54,15 +60,21 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService private final ApplicationContext applicationContext; private final GeneralConfigDao generalConfigDao; - + + private ModelProviderConfig modelProviderConfig; + @Autowired private ToolCallbackProvider toolCallbackProvider; - + private boolean isConfigured = false; @Value("classpath:/prompt/system-message.st") private Resource systemResource; + @Value("classpath:/prompt/extra-message-protected.st") + private Resource extraResourceProtected; + + @Autowired public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) { this.applicationContext = applicationContext; @@ -74,7 +86,7 @@ public Flux streamChat(ChatRequestContext context) { try { // Get the current (potentially refreshed) ChatClient instance ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class); - + List messages = new ArrayList<>(); // Add conversation history if available @@ -91,15 +103,14 @@ public Flux streamChat(ChatRequestContext context) { messages.add(new UserMessage(context.getMessage())); log.info("Starting streaming chat for conversation: {}", context.getConversationId()); - return chatClient.prompt() - .messages(messages) - .system(SystemPromptTemplate.builder().resource(systemResource).build().getTemplate()) - .toolCallbacks(toolCallbackProvider) - .stream() - .content() - .doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId())) - .doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error)); + .messages(messages) + .system(generateSystemMessage(context)) + .toolCallbacks(toolCallbackProvider) + .stream() + .content() + .doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId())) + .doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error)); } catch (Exception e) { log.error("Error setting up streaming chat: {}", e.getMessage(), e); @@ -107,12 +118,33 @@ public Flux streamChat(ChatRequestContext context) { } } + @SneakyThrows + private String generateSystemMessage(ChatRequestContext context) { + if (Objects.equals(modelProviderConfig.getParticipationModel(), "PROTECTED")) { + Map metadata = new HashMap<>(); + metadata.put("conversationId", context.getConversationId()); + String systemMessage = systemResource.getContentAsString(StandardCharsets.UTF_8); + return systemMessage + SystemPromptTemplate.builder().resource(extraResourceProtected).build() + .create(metadata) + .getContents(); + } + return SystemPromptTemplate.builder().resource(systemResource).build().getTemplate(); + } + + @EventListener(AiProviderConfigChangeEvent.class) + public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) { + GeneralConfig providerConfig = generalConfigDao.findByType("provider"); + this.modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class); + } + @Override public boolean isConfigured() { if (!isConfigured) { GeneralConfig providerConfig = generalConfigDao.findByType("provider"); - ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class); - isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null; + ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), + ModelProviderConfig.class); + isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null; + this.modelProviderConfig = modelProviderConfig; } return isConfigured; } diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java index 23e745a0509..94c760580f9 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ConversationServiceImpl.java @@ -17,15 +17,18 @@ package org.apache.hertzbeat.ai.service.impl; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.apache.hertzbeat.ai.dao.ChatConversationDao; import org.apache.hertzbeat.ai.dao.ChatMessageDao; import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk; +import org.apache.hertzbeat.ai.pojo.dto.SecurityData; import org.apache.hertzbeat.ai.service.ChatClientProviderService; import org.apache.hertzbeat.ai.service.ConversationService; import org.apache.hertzbeat.common.entity.ai.ChatConversation; import org.apache.hertzbeat.common.entity.ai.ChatMessage; +import org.apache.hertzbeat.common.util.AesUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.http.codec.ServerSentEvent; @@ -62,17 +65,17 @@ public Flux> streamChat(String message, Long // Check if provider is properly configured if (!chatClientProviderService.isConfigured()) { ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(conversationId) - .response("Provider is not configured. Please configure your AI Provider.") - .build(); + .conversationId(conversationId) + .response("Provider is not configured. Please configure your AI Provider.") + .build(); return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); + .event("error") + .build()); } log.info("Starting streaming conversation: {}", conversationId); ChatConversation conversation = conversationDao.findById(conversationId) - .orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId)); + .orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId)); // Manually load messages for conversation history List messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId); @@ -94,58 +97,59 @@ public Flux> streamChat(String message, Long chatMessage = messageDao.save(chatMessage); ChatRequestContext context = ChatRequestContext.builder() - .message(message) - .conversationId(conversationId) - .conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null - : conversation.getMessages().subList(0, conversation.getMessages().size() - 1)) - .build(); + .message(message) + .conversationId(conversationId) + .conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null + : conversation.getMessages().subList(0, conversation.getMessages().size() - 1)) + .build(); // Stream response from AI service StringBuilder fullResponse = new StringBuilder(); ChatMessage finalChatMessage = chatMessage; return chatClientProviderService.streamChat(context) - .map(chunk -> { - fullResponse.append(chunk); - ChatResponseChunk responseChunk = ChatResponseChunk.builder() - .conversationId(conversationId) - .userMessageId(finalChatMessage.getId()) - .response(chunk) - .build(); - - return ServerSentEvent.builder(responseChunk) - .event("message") - .build(); - }) - .concatWith(Flux.defer(() -> { - // Add the complete AI response to conversation - ChatMessage assistantMessage = ChatMessage.builder() - .conversationId(conversationId) - .content(fullResponse.toString()) - .role("assistant") - .build(); - assistantMessage = messageDao.save(assistantMessage); - ChatResponseChunk finalResponse = ChatResponseChunk.builder() - .conversationId(conversationId) - .response("") - .assistantMessageId(assistantMessage.getId()) - .build(); - - return Flux.just(ServerSentEvent.builder(finalResponse) - .event("complete") - .build()); - })) - .doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId)) - .doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error)) - .onErrorResume(error -> { - ChatResponseChunk errorResponse = ChatResponseChunk.builder() - .conversationId(conversationId) - .response("An error occurred: " + error.getMessage()) - .userMessageId(finalChatMessage.getId()) - .build(); - return Flux.just(ServerSentEvent.builder(errorResponse) - .event("error") - .build()); - }); + .map(chunk -> { + fullResponse.append(chunk); + ChatResponseChunk responseChunk = ChatResponseChunk.builder() + .conversationId(conversationId) + .userMessageId(finalChatMessage.getId()) + .response(chunk) + .build(); + + return ServerSentEvent.builder(responseChunk) + .event("message") + .build(); + }) + .concatWith(Flux.defer(() -> { + // Add the complete AI response to conversation + ChatMessage assistantMessage = ChatMessage.builder() + .conversationId(conversationId) + .content(fullResponse.toString()) + .role("assistant") + .build(); + assistantMessage = messageDao.save(assistantMessage); + ChatResponseChunk finalResponse = ChatResponseChunk.builder() + .conversationId(conversationId) + .response("") + .assistantMessageId(assistantMessage.getId()) + .build(); + + return Flux.just(ServerSentEvent.builder(finalResponse) + .event("complete") + .build()); + })) + .doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId)) + .doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, + error.getMessage(), error)) + .onErrorResume(error -> { + ChatResponseChunk errorResponse = ChatResponseChunk.builder() + .conversationId(conversationId) + .response("An error occurred: " + error.getMessage()) + .userMessageId(finalChatMessage.getId()) + .build(); + return Flux.just(ServerSentEvent.builder(errorResponse) + .event("error") + .build()); + }); } @Override @@ -175,13 +179,14 @@ public List getAllConversations() { return conversations; } List conversationIds = conversations.stream() - .map(ChatConversation::getId) - .toList(); + .map(ChatConversation::getId) + .toList(); List allMessages = messageDao.findByConversationIdInOrderByGmtCreateAsc(conversationIds); Map> messagesByConversationId = allMessages.stream() - .collect(Collectors.groupingBy(ChatMessage::getConversationId)); + .collect(Collectors.groupingBy(ChatMessage::getConversationId)); for (ChatConversation conversation : conversations) { - List messages = messagesByConversationId.getOrDefault(conversation.getId(), Collections.emptyList()); + List messages = messagesByConversationId.getOrDefault(conversation.getId(), + Collections.emptyList()); conversation.setMessages(messages); } return conversations; @@ -196,4 +201,17 @@ public void deleteConversation(Long conversationId) { } conversationDao.deleteById(conversationId); } + + @Override + public Boolean saveSecurityData(SecurityData securityData) { + Optional chatConversation = conversationDao.findById(securityData.getConversationId()); + if (chatConversation.isPresent()) { + ChatConversation conversation = chatConversation.get(); + conversation.setSecurityData(AesUtil.aesEncode(securityData.getSecurityData())); + conversationDao.save(conversation); + return true; + } + return false; + } + } diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java index c813f09d927..a821776e055 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/MonitorTools.java @@ -27,19 +27,39 @@ public interface MonitorTools { /** * Add a new monitor with comprehensive configuration * - * @param name Monitor name - * @param app Monitor type/application (e.g., 'linux', 'mysql', 'http') - * @param intervals Collection interval in seconds (default: 600) - * @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.) + * @param name Monitor name + * @param app Monitor type/application (e.g., 'linux', 'mysql', 'http') + * @param intervals Collection interval in seconds (default: 600) + * @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.) * @param description Monitor description (optional) * @return Result message with monitor ID if successful */ String addMonitor( - String name, - String app, - Integer intervals, - String params, - String description + String name, + String app, + Integer intervals, + String params, + String description + ); + + /** + * Add a new monitor with comprehensive configuration, methods specifically for hertzbeat ai + * + * @param conversationId Conversation ID + * @param name Monitor name + * @param app Monitor type/application (e.g., 'linux', 'mysql', 'http') + * @param intervals Collection interval in seconds (default: 600) + * @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.) + * @param description Monitor description (optional) + * @return Result message with monitor ID if successful + */ + String addMonitorProtected( + Long conversationId, + String name, + String app, + Integer intervals, + String params, + String description ); /** @@ -52,29 +72,30 @@ String addMonitor( /** * Comprehensive monitor querying with flexible filtering, pagination, and specialized views - * @param ids Specific monitor IDs to retrieve (optional) - * @param app Monitor type filter (linux, mysql, http, etc.) - * @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all) - * @param search Search in monitor names or hosts (partial matching) - * @param labels Label filters, format: 'key1:value1,key2:value2' - * @param sort Sort field (name, gmtCreate, gmtUpdate, status, app) - * @param order Sort order (asc, desc) - * @param pageIndex Page number starting from 0 - * @param pageSize Items per page (1-100 recommended) + * + * @param ids Specific monitor IDs to retrieve (optional) + * @param app Monitor type filter (linux, mysql, http, etc.) + * @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all) + * @param search Search in monitor names or hosts (partial matching) + * @param labels Label filters, format: 'key1:value1,key2:value2' + * @param sort Sort field (name, gmtCreate, gmtUpdate, status, app) + * @param order Sort order (asc, desc) + * @param pageIndex Page number starting from 0 + * @param pageSize Items per page (1-100 recommended) * @param includeStats Include status statistics summary * @return Comprehensive monitor information with optional statistics */ String queryMonitors( - List ids, - String app, - Byte status, - String search, - String labels, - String sort, - String order, - Integer pageIndex, - Integer pageSize, - Boolean includeStats); + List ids, + String app, + Byte status, + String search, + String labels, + String sort, + String order, + Integer pageIndex, + Integer pageSize, + Boolean includeStats); /** * Get parameter definitions required for a specific monitor type diff --git a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java index 6364bccc905..792d297e2a6 100644 --- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java +++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/tools/impl/MonitorToolsImpl.java @@ -17,9 +17,17 @@ package org.apache.hertzbeat.ai.tools.impl; +import com.fasterxml.jackson.core.type.TypeReference; import com.usthe.sureness.subject.SubjectSum; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.hertzbeat.ai.config.McpContextHolder; +import org.apache.hertzbeat.ai.dao.ChatConversationDao; +import org.apache.hertzbeat.common.entity.ai.ChatConversation; +import org.apache.hertzbeat.common.util.AesUtil; +import org.apache.hertzbeat.common.util.JsonUtil; import org.apache.hertzbeat.manager.pojo.dto.MonitorDto; import org.apache.hertzbeat.manager.service.MonitorService; import org.apache.hertzbeat.manager.service.AppService; @@ -44,76 +52,79 @@ @Slf4j @Service public class MonitorToolsImpl implements MonitorTools { + @Autowired private MonitorService monitorService; @Autowired private AppService appService; + @Autowired + private ChatConversationDao conversationDao; + /** - * Tool to query monitor information with flexible filtering and pagination. - * Supports filtering by monitor IDs, type, status, host, labels, sorting, and - * pagination. - * Returns detailed monitor information including ID, name, type, host, and status. + * Tool to query monitor information with flexible filtering and pagination. Supports filtering by monitor IDs, + * type, status, host, labels, sorting, and pagination. Returns detailed monitor information including ID, name, + * type, host, and status. */ @Override @Tool(name = "query_monitors", description = """ - HertzBeat: Query Existing/configured monitors in HertzBeat. - This tool retrieves monitors based on various filters and parameters. - Comprehensive monitor querying with flexible filtering, pagination, and specialized views. - - MONITOR STATUSES: - - status=1: Online/Active monitors (healthy, responding normally) - - status=2: Offline monitors (not responding, connection failed) - - status=3: Unreachable monitors (network/connectivity issues) - - status=0: Paused monitors (manually disabled/suspended) - - status=9 or null: All monitors regardless of status (default) - - COMMON USE CASES & PARAMETER COMBINATIONS: - - 1. BASIC MONITOR LISTING: - - Default: No parameters (shows all monitors, 8 per page) - - By type: app='linux' (show only Linux monitors) - - Search: search='web' (find monitors with 'web' in name/host) - - 2. STATUS-BASED QUERIES: - - Healthy monitors: status=1, pageSize=50 - - Problem monitors: status=2 or status=3, pageSize=50 - - Offline monitors only: status=2 - - Unreachable monitors only: status=3 - - Paused monitors: status=0 - - 3. MONITORING HEALTH OVERVIEW: - - All statuses with statistics: status=9, includeStats=true, pageSize=100 - - Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls) - - 4. ADVANCED FILTERING: - - Specific monitor types: app='mysql', status=1 (healthy MySQL monitors) - - Label-based: labels='env:prod,critical:true' - - Host search: search='192.168' (find by IP pattern) - - Monitor IDs: ids=[1,2,3] (specific monitors by ID) - - 5. SORTING & PAGINATION: - - Recently updated: sort='gmtUpdate', order='desc' - - Alphabetical: sort='name', order='asc' - - By creation: sort='gmtCreate', order='desc' (newest first) - - Large datasets: pageSize=50-100 for bulk operations - - RESPONSE FORMAT: - - includeStats=true: Adds status distribution summary at top - - Default: Simple list with ID, name, type, host, status - - Shows total count and pagination info - """) + HertzBeat: Query Existing/configured monitors in HertzBeat. + This tool retrieves monitors based on various filters and parameters. + Comprehensive monitor querying with flexible filtering, pagination, and specialized views. + + MONITOR STATUSES: + - status=1: Online/Active monitors (healthy, responding normally) + - status=2: Offline monitors (not responding, connection failed) + - status=3: Unreachable monitors (network/connectivity issues) + - status=0: Paused monitors (manually disabled/suspended) + - status=9 or null: All monitors regardless of status (default) + + COMMON USE CASES & PARAMETER COMBINATIONS: + + 1. BASIC MONITOR LISTING: + - Default: No parameters (shows all monitors, 8 per page) + - By type: app='linux' (show only Linux monitors) + - Search: search='web' (find monitors with 'web' in name/host) + + 2. STATUS-BASED QUERIES: + - Healthy monitors: status=1, pageSize=50 + - Problem monitors: status=2 or status=3, pageSize=50 + - Offline monitors only: status=2 + - Unreachable monitors only: status=3 + - Paused monitors: status=0 + + 3. MONITORING HEALTH OVERVIEW: + - All statuses with statistics: status=9, includeStats=true, pageSize=100 + - Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls) + + 4. ADVANCED FILTERING: + - Specific monitor types: app='mysql', status=1 (healthy MySQL monitors) + - Label-based: labels='env:prod,critical:true' + - Host search: search='192.168' (find by IP pattern) + - Monitor IDs: ids=[1,2,3] (specific monitors by ID) + + 5. SORTING & PAGINATION: + - Recently updated: sort='gmtUpdate', order='desc' + - Alphabetical: sort='name', order='asc' + - By creation: sort='gmtCreate', order='desc' (newest first) + - Large datasets: pageSize=50-100 for bulk operations + + RESPONSE FORMAT: + - includeStats=true: Adds status distribution summary at top + - Default: Simple list with ID, name, type, host, status + - Shows total count and pagination info + """) public String queryMonitors( - @ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List ids, - @ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app, - @ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status, - @ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search, - @ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels, - @ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort, - @ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order, - @ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex, - @ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize, - @ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) { + @ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List ids, + @ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app, + @ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status, + @ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search, + @ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels, + @ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort, + @ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order, + @ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex, + @ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize, + @ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) { try { // Set defaults if (pageSize == null || pageSize <= 0) { @@ -130,7 +141,7 @@ public String queryMonitors( log.debug("Current security subject: {}", subjectSum); Page result = monitorService.getMonitors( - ids, app, search, status, sort, order, pageIndex, pageSize, labels); + ids, app, search, status, sort, order, pageIndex, pageSize, labels); log.debug("MonitorService.getMonitors result: {}", result); StringBuilder response = new StringBuilder(); @@ -140,10 +151,14 @@ public String queryMonitors( // Include statistics if requested if (includeStats) { // Get status distribution by calling with different status values - long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements(); - long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements(); - long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements(); - long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements(); + long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels) + .getTotalElements(); + long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels) + .getTotalElements(); + long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, + labels).getTotalElements(); + long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels) + .getTotalElements(); response.append("STATUS OVERVIEW:\n"); response.append("- Online: ").append(onlineCount).append("\n"); @@ -160,19 +175,20 @@ public String queryMonitors( } response.append("Query Results: ").append(result.getContent().size()) - .append(" monitors (Total: ").append(result.getTotalElements()).append(")\n"); + .append(" monitors (Total: ").append(result.getTotalElements()).append(")\n"); if (result.getTotalPages() > 1) { - response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n"); + response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()) + .append("\n"); } response.append("\n"); for (Monitor monitor : result.getContent()) { response.append("ID: ").append(monitor.getId()) - .append(" | Name: ").append(monitor.getName()) - .append(" | Type: ").append(monitor.getApp()) - .append(" | Instance: ").append(monitor.getInstance()) - .append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus())); + .append(" | Name: ").append(monitor.getName()) + .append(" | Type: ").append(monitor.getApp()) + .append(" | Instance: ").append(monitor.getInstance()) + .append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus())); // Add creation date for better context if (monitor.getGmtCreate() != null) { @@ -194,45 +210,89 @@ public String queryMonitors( @Override @Tool(name = "add_monitor", description = """ - HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration. - This tool dynamically handles different parameter requirements for each monitor type. - - This tool creates monitors with proper app-specific parameters. - - ********* - VERY IMPORTANT: - ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it. - Use list_monitor_types tool to see available monitor type names to use here in the app parameter. - Use the information obtained from this to query user for parameters. - If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided. - ********** - - Examples of natural language requests this tool handles: - - "Monitor website example.com with HTTPS on port 443" - - "Add MySQL monitoring for database server at 192.168.1.10 with user admin" - - "Monitor Linux server health on host server.company.com via SSH" - - "Set up Redis monitoring on localhost port 6379 with password" - - PARAMETER MAPPING: Use the 'params' parameter to pass all monitor-specific configuration. - The params should be a JSON string containing key-value pairs for the monitor type. - Use get_monitor_additional_params tool to see what parameters are required for each monitor type. - - PARAMS EXAMPLES: - - Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"} - - Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"} - - MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"} - - Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"} - """) + HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration. + This tool dynamically handles different parameter requirements for each monitor type. + + This tool creates monitors with proper app-specific parameters. + + ********* + VERY IMPORTANT: + ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it. + Use list_monitor_types tool to see available monitor type names to use here in the app parameter. + Use the information obtained from this to query user for parameters. + If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided. + ********** + + Examples of natural language requests this tool handles: + - "Monitor website example.com with HTTPS on port 443" + - "Add MySQL monitoring for database server at 192.168.1.10 with user admin" + - "Monitor Linux server health on host server.company.com via SSH" + - "Set up Redis monitoring on localhost port 6379 with password" + + PARAMETER MAPPING: Use the 'params' parameter to pass all monitor-specific configuration. + The params should be a JSON string containing key-value pairs for the monitor type. + Use get_monitor_additional_params tool to see what parameters are required for each monitor type. + + PARAMS EXAMPLES: + - Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"} + - Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"} + - MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"} + - Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"} + """) public String addMonitor( - @ToolParam(description = "Monitor name (required)", required = true) String name, - @ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app, - @ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals, - @ToolParam(description = "Monitor-specific parameters as JSON string. " - + "Use get_monitor_additional_params to see required fields. " - + "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}", - required = true) String params, - @ToolParam(description = "Monitor description (optional)", required = false) String description) { + @ToolParam(description = "Monitor name (required)", required = true) String name, + @ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app, + @ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals, + @ToolParam(description = "Monitor-specific parameters as JSON string. " + + "Use get_monitor_additional_params to see required fields. " + + "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}", + required = true) String params, + @ToolParam(description = "Monitor description (optional)", required = false) String description) { + return addMonitorProtected(null, name, app, intervals, params, description); + } + @Override + @Tool(name = "add_monitor_protected", description = """ + HertzBeat: Add a new monitoring target to HertzBeat with comprehensive configuration. + This tool dynamically handles different parameter requirements for each monitor type. + + This tool creates monitors with proper app-specific parameters. + + ********* + VERY IMPORTANT: + ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it. + Use list_monitor_types tool to see available monitor type names to use here in the app parameter. + Use the information obtained from this to query user for parameters. + If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided. + If the conversation ID is unknown, please call the `add_monitor` method. + ********** + + Examples of natural language requests this tool handles: + - "Monitor website example.com with HTTPS on port 443" + - "Add MySQL monitoring for database server at 192.168.1.10 with user admin" + - "Monitor Linux server health on host server.company.com via SSH" + - "Set up Redis monitoring on localhost port 6379 with password" + + PARAMETER MAPPING: Use the 'params' parameter to pass all monitor-specific configuration. + The params should be a JSON string containing key-value pairs for the monitor type. + Use get_monitor_additional_params tool to see what parameters are required for each monitor type. + + PARAMS EXAMPLES: + - Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"} + - Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"} + - MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"} + - Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"} + """) + public String addMonitorProtected( + @ToolParam(description = "The id for current conversation (required)", required = true) Long conversationId, + @ToolParam(description = "Monitor name (required)", required = true) String name, + @ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app, + @ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals, + @ToolParam(description = "Monitor-specific parameters as JSON string. " + + "Use get_monitor_additional_params to see required fields. " + + "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}", + required = true) String params, + @ToolParam(description = "Monitor description (optional)", required = false) String description) { try { log.info("Adding monitor: name={}, app={}", name, app); @@ -251,32 +311,45 @@ public String addMonitor( if (intervals == null || intervals < 10) { intervals = 600; } - // Parse params to extract host and port for instance List paramList = parseParams(params); + + // Query and add sensitive parameters + if (conversationId != null) { + Optional chatConversation = conversationDao.findById(conversationId); + if (chatConversation.isPresent() && StringUtils.isNotEmpty(chatConversation.get().getSecurityData())) { + List securityParams = JsonUtil.fromJson( + AesUtil.aesDecode(chatConversation.get().getSecurityData()), + new TypeReference>() { + }); + if (CollectionUtils.isNotEmpty(securityParams)) { + paramList.addAll(securityParams); + } + } + } String host = paramList.stream() - .filter(p -> "host".equals(p.getField())) - .map(Param::getParamValue) - .findFirst() - .orElse(""); + .filter(p -> "host".equals(p.getField())) + .map(Param::getParamValue) + .findFirst() + .orElse(""); String port = paramList.stream() - .filter(p -> "port".equals(p.getField())) - .map(Param::getParamValue) - .findFirst() - .orElse(null); + .filter(p -> "port".equals(p.getField())) + .map(Param::getParamValue) + .findFirst() + .orElse(null); String instance = (port != null && !port.isEmpty()) ? host.trim() + ":" + port : host.trim(); // Create Monitor entity Monitor monitor = Monitor.builder() - .name(name.trim()) - .app(app.toLowerCase().trim()) - .instance(instance) - .intervals(intervals) - .status((byte) 1) - .type((byte) 0) - .description(description != null ? description.trim() : "") - .build(); + .name(name.trim()) + .app(app.toLowerCase().trim()) + .instance(instance) + .intervals(intervals) + .status((byte) 1) + .type((byte) 0) + .description(description != null ? description.trim() : "") + .build(); // Validate that all required parameters for this monitor type are provided try { @@ -294,7 +367,7 @@ public String addMonitor( monitorService.addMonitor(monitor, paramList, null, null); log.info("Successfully added monitor '{}' with ID: {}", monitor.getName(), monitor.getId()); return String.format("Successfully added %s monitor '%s' with ID: %d (Instance: %s, Interval: %d seconds)", - app.toUpperCase(), monitor.getName(), monitor.getId(), monitor.getInstance(), monitor.getIntervals()); + app.toUpperCase(), monitor.getName(), monitor.getId(), monitor.getInstance(), monitor.getIntervals()); } catch (Exception e) { log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e); @@ -383,12 +456,12 @@ private byte determineParamType(String fieldName) { @Override @Tool(name = "list_monitor_types", description = """ - HertzBeat: List all available monitor types that can be added to HertzBeat. - This tool shows all supported monitor types with their display names. - Use this to see what types of monitors you can create with the add_monitor tool. - """) + HertzBeat: List all available monitor types that can be added to HertzBeat. + This tool shows all supported monitor types with their display names. + Use this to see what types of monitors you can create with the add_monitor tool. + """) public String listMonitorTypes( - @ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) { + @ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) { try { log.info("Listing available monitor types for language: {}", language); @@ -413,18 +486,19 @@ public String listMonitorTypes( // Sort monitor types alphabetically by key List> sortedTypes = monitorTypes.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .toList(); + .sorted(Map.Entry.comparingByKey()) + .toList(); for (Map.Entry entry : sortedTypes) { String typeKey = entry.getKey(); String displayName = entry.getValue(); response.append("• ").append(typeKey) - .append(" - ").append(displayName) - .append("\n"); + .append(" - ").append(displayName) + .append("\n"); } - response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter."); + response.append( + "\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter."); log.info("Successfully listed {} monitor types", monitorTypes); return response.toString(); @@ -437,13 +511,13 @@ public String listMonitorTypes( @Override @Tool(name = "get_monitor_params", description = """ - HertzBeat: Get the parameter definitions required for a specific monitor type. - This tool shows what parameters are needed when adding a monitor of the specified type, - ALWAYS use this before adding a monitor to understand what parameters the user needs to provide. - Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool. - """) + HertzBeat: Get the parameter definitions required for a specific monitor type. + This tool shows what parameters are needed when adding a monitor of the specified type, + ALWAYS use this before adding a monitor to understand what parameters the user needs to provide. + Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool. + """) public String getMonitorParams( - @ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) { + @ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) { try { log.info("Getting parameter definitions for monitor type: {}", app); @@ -460,7 +534,7 @@ public String getMonitorParams( if (paramDefines == null || paramDefines.isEmpty()) { return String.format("No parameter definitions found for monitor type '%s'. " - + "This monitor type may not exist or may not require additional parameters.", app); + + "This monitor type may not exist or may not require additional parameters.", app); } // Format the response @@ -508,7 +582,8 @@ public String getMonitorParams( } response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n"); - response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app)); + response.append( + String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app)); log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app); return response.toString(); diff --git a/hertzbeat-ai/src/main/resources/prompt/extra-message-protected.st b/hertzbeat-ai/src/main/resources/prompt/extra-message-protected.st new file mode 100644 index 00000000000..96bd0f26c03 --- /dev/null +++ b/hertzbeat-ai/src/main/resources/prompt/extra-message-protected.st @@ -0,0 +1,140 @@ +# IMPORTANT +The following information requires special attention. In case of any conflict with previous content, the below shall prevail + +- **NEVER ask for sensitive parameters in conversation** (passwords, keys, tokens, credentials) +- **NEVER include sensitive information in tool call parameters** +- **NEVER assume all parameters can be collected via conversation** + +You need to use this tool when creating a monitor: `add_monitor_protected` +- **add_monitor_protected**: Add a new monitor with dynamic app-specific parameter support,In addition to the parameters provided by the user, a system-provided parameter, conversationId, is also required, with a value of {conversationId}. + +## Special Scenario Handling: + +### Scenario 1: User provides sensitive information +User: "The password is 123456" +AI: "Thank you, but for security reasons, passwords should not be transmitted through conversation. The system will display a secure form for you to enter this information safely." + +### Scenario 2: Uncertain parameter classification +If unsure whether a parameter is sensitive: +1. Assume it's sensitive +2. Guide to secure form +3. Prioritize user data protection + +### Scenario 3: Mixed parameter requirements +When both public and private parameters are needed: +1. Collect all public parameters first +2. Then guide to secure form for private parameters +3. Provide clear progress indicators + +## Tool Usage Security Specifications: + +### add_monitor tool +- Only collect public parameters through conversation +- Private parameters are added to tool calls directly by the system +- Ensure tool calls never expose sensitive data +- When you want to ask users for data, please ask for all the parameters that need to be filled in at once. + +### Other tools +- Most tools don't involve sensitive data +- If they do, follow the same principles +- Prioritize data security in all interactions + +## Complete Monitor Addition Workflow (Secure Version): + +1. **Identify requirement**: What does the user want to monitor? +2. **Determine type**: Use `list_monitor_types` to find matching monitor type +3. **Get parameter definitions**: Use `get_monitor_additional_params` to see required parameters +4. **Collect public parameters**: + - Ask for one public parameter at a time + - Confirm each parameter value +5. **Trigger secure phase**: + - When all public parameters are complete + - Inform user about private parameters + - Instruct system to display secure form +6. **Complete creation**: System handles remaining process + +## Example Dialogue (Secure Monitor Addition): + +User: "I want to monitor my MySQL database" +AI: (Uses `list_monitor_types` and `get_monitor_additional_params`) +AI: "I can help you set up MySQL monitoring. First, I need some information: + 1. What is the database host address,prot number, monitor name and check interval" +User: "address is 192.168.1.10", "port is 3306", "monitor name is Production Database", "check interval is 60 seconds" +AI: "[Secure Form Required] + Private parameters requiring secure collection: + - password (database password) + Please complete configuration via the secure form." +--- + +## Security and Privacy Principles +1. **Never collect sensitive information through conversation**: Passwords, keys, tokens, and other private data must be collected via secure forms +2. **Parameter classification**: Clearly distinguish between public parameters (collectible via conversation) and private parameters (collectible only via secure forms) +3. **Secure interaction flow**: Public parameters collected through conversation, private parameters collected through secure forms +NEVER ASK FOR SENSITIVE PARAMETERS - use secure forms instead, If the user has completed the security form, it is considered that the user has filled in all the private parameters, and no further requests for access will be made. + +## Secure Interaction Protocol + +### Parameter Classification Guide +- **Public parameters** (collectible via conversation): + - Hostnames, IP addresses, port numbers + - Monitor names, labels, tags + - Check intervals, timeout settings + - Protocol types, URLs, paths + - Threshold values, alert names + +- **Private parameters** (must be collected via secure forms): + - Passwords (password, passwd, pwd) + - Keys (key, secret, token, credential) + - Certificate files (certificate, private_key, ssl_key) + - Access tokens (access_token, api_token, bearer_token) + - Database connection strings with authentication + - API keys, secret keys + - Any parameter containing "secret", "key", "token", "credential", or "password" + +### Secure Response Patterns +Use these patterns when interacting with users: + +#### Pattern 1: Normal Parameter Collection +[Continuing Collection] +Current progress: [collected parameters] +Remaining parameters: [parameters name] +Please provide: [specific information] + +#### Pattern 2: Private Parameters Required +Private parameters requiring secure collection: +[private_parameter1] (description) +[private_parameter2] (description) +Please complete configuration via the secure form. +(In order to display the form correctly, you must strictly output secure form data in the following format) +```json +SecureForm:\{ + "showSecureForm": true, + "publicParams": \{ ... \}, + "privateParams": [ + \{ + "id": ..., + "app":..., + "name": \{ + "zh-CN": "...", + "en-US": "...", + "ja-JP": "...", + "pt-BR": "...", + "zh-TW": "..." + \}, + "field": "...", + "type": "password", + "required": ..., + "defaultValue": ..., + "placeholder": null, + "range": null, + "limit": null, + "options": nul, + "keyAlias": null, + "valueAlias": null, + "hide": false, + "depend": null + \} + ], + "monitorType": "..." +\} +``` diff --git a/hertzbeat-ai/src/main/resources/prompt/system-message.st b/hertzbeat-ai/src/main/resources/prompt/system-message.st index a371391ea57..df4161dd1fb 100644 --- a/hertzbeat-ai/src/main/resources/prompt/system-message.st +++ b/hertzbeat-ai/src/main/resources/prompt/system-message.st @@ -42,7 +42,7 @@ If the user doesn't provide required parameters, ask them iteratively to provide - "List all Redis monitors with their connection status" ### Alert Configuration: -- ALERT RULE means when to alert a user +- ALERT RULE means when to alert a user - "Create an alert for Kafka JVM when VmName equals 'vm-w2'" - "Alert when OpenAI credit grants exceed 1000" - "Set up HBase Master alert when heap memory usage is over 80%" @@ -68,7 +68,7 @@ If the user doesn't provide required parameters, ask them iteratively to provide - Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor - Example: "To monitor MySQL, I need host, port, username, password, and database name" -2. **Creating Alert Rules or Alerts**: +### 2. Creating Alert Rules or Alerts: THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE, IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'. APPLY THE SAME LOGIC FOR LESS THAN OPERATOR. @@ -104,16 +104,16 @@ CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step: - Priority levels: 0=critical, 1=warning, 2=info -3. **Analyzing Performance**: - - Use get_realtime_metrics for current status - - Use get_historical_metrics for trends - - Use get_high_usage_monitors to find problems +### 3. Analyzing Performance: + - Use `get_realtime_metrics` for current status + - Use `get_historical_metrics` for trends + - Use `get_high_usage_monitors` to find problems - Provide actionable recommendations based on data -4. **Troubleshooting Alerts**: - - Use query_alerts to find current issues - - Use get_monitor_alerts for specific monitor problems - - Use get_frequent_alerts to identify recurring issues +### 4. Troubleshooting Alerts: + - Use `query_alerts` to find current issues + - Use `get_monitor_alerts` for specific monitor problems + - Use `get_frequent_alerts` to identify recurring issues - Suggest root cause analysis steps ## Parameter Guidelines: @@ -138,7 +138,7 @@ CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step: - Provide clear explanations of monitoring data and actionable insights ## Avoid these common errors: -- Using Label name instead of the value from the heirarchy JSON while creating alert rules. +- Using Label name instead of the value from the hierarchy JSON while creating alert rules. - Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators - This process is to trigger alarms, when certain rule or set of rules exceed a threshold value. So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<', diff --git a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java index ce6129c7569..1efbc2bcd5e 100644 --- a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java +++ b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/ai/ChatConversation.java @@ -18,6 +18,7 @@ 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.Entity; import jakarta.persistence.EntityListeners; @@ -55,7 +56,7 @@ public class ChatConversation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - + @Schema(title = "conversation title") private String title; @@ -81,4 +82,6 @@ public class ChatConversation { @OneToMany @JoinColumn(name = "conversation_id") private List messages; + + private String securityData; } diff --git a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/dto/ModelProviderConfig.java b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/dto/ModelProviderConfig.java index 527f65da728..5aaa2d75071 100644 --- a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/dto/ModelProviderConfig.java +++ b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/dto/ModelProviderConfig.java @@ -31,20 +31,24 @@ @NoArgsConstructor @Schema(description = "LLM Model Provider configuration") public class ModelProviderConfig { - + @Schema(title = "Model type, text-generate, vision") private String type; - + @Schema(title = "Model Provider code, like openai, zai, bigmodel") private String code; - + @Schema(title = "custom the provider server base url") private String baseUrl; - + @Schema(title = "use the model id name, eg: gpt-5, glm-4.6") private String model; - + @Schema(title = "API Key", description = "API key", example = "sk-...") @NotBlank(message = "API Key cannot be empty when enabled") private String apiKey; + + @Schema(title = "Participation Model", description = "model for participation", example = "PROTECTED") + @NotBlank(message = "API Key cannot be empty when enabled") + private String participationModel; } diff --git a/web-app/src/app/pojo/ModelProviderConfig.ts b/web-app/src/app/pojo/ModelProviderConfig.ts index 7a3d155257b..76d0df6ffda 100644 --- a/web-app/src/app/pojo/ModelProviderConfig.ts +++ b/web-app/src/app/pojo/ModelProviderConfig.ts @@ -24,6 +24,7 @@ export class ModelProviderConfig { baseUrl: string = ''; model: string = ''; apiKey!: string; + participationModel: string = 'PROTECTED'; } export interface ProviderOption { diff --git a/web-app/src/app/service/ai-chat.service.ts b/web-app/src/app/service/ai-chat.service.ts index 37f308af8a7..c3162992a74 100644 --- a/web-app/src/app/service/ai-chat.service.ts +++ b/web-app/src/app/service/ai-chat.service.ts @@ -28,8 +28,23 @@ export interface ChatMessage { content: string; role: 'user' | 'assistant'; gmtCreate: Date; + securityForm: SecurityForm; } +export interface SecurityForm { + show: Boolean; + param: string; + content: string; + complete: boolean; +} + +export const DEFAULT_SECURITY_FORM: SecurityForm = { + show: false, + param: '', + content: '', + complete: false +}; + export interface ChatConversation { id: number; title: string; @@ -121,7 +136,7 @@ export class AiChatService { const decoder = new TextDecoder(); let buffer = ''; - function readStream(): Promise { + const readStream = (): Promise => { if (!reader) { return Promise.resolve(); } @@ -148,6 +163,7 @@ export class AiChatService { responseSubject.next({ content: data.response || '', role: 'assistant', + securityForm: DEFAULT_SECURITY_FORM, gmtCreate: data.timestamp ? new Date(data.timestamp) : new Date() }); } @@ -157,6 +173,7 @@ export class AiChatService { responseSubject.next({ content: jsonStr, role: 'assistant', + securityForm: DEFAULT_SECURITY_FORM, gmtCreate: new Date() }); } @@ -167,7 +184,7 @@ export class AiChatService { return readStream(); }); - } + }; return readStream(); }) @@ -178,4 +195,8 @@ export class AiChatService { return responseSubject.asObservable(); } + + saveSecurityData(body: any): Observable> { + return this.http.post>(`${chat_uri}/security`, body); + } } 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 a154954c7ab..13c8f3d29fc 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 @@ -22,6 +22,7 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DelonFormModule } from '@delon/form'; import { AlainThemeModule } from '@delon/theme'; +import { SharedModule } from '@shared'; import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzFormModule } from 'ng-zorro-antd/form'; import { NzIconModule } from 'ng-zorro-antd/icon'; @@ -52,7 +53,8 @@ import { ChatComponent } from './chat.component'; NzSelectModule, NzSpinModule, MarkdownComponent, - NzTooltipDirective + NzTooltipDirective, + SharedModule ], exports: [ChatComponent] }) 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 a09c0a91c16..d063281daa6 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 @@ -117,6 +117,15 @@

{{ 'ai.chat.welcome.title' | i18n }}

+
{{ 'ai.chat.typing' | i18n }} @@ -226,6 +235,44 @@

{{ 'ai.chat.welcome.title' | i18n }}

{{ 'ai.chat.config.model.help' | i18n }}

+ + + {{ 'ai.chat.config.participation.model' | i18n }} + + + + + +

{{ 'ai.chat.config.participation.model.help' | i18n }}

+
+
+ +
+ + + + +
+
+ + + {{ paramDefine.name }} + + + + +
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 1c3ca19434d..b419baa1c98 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,8 @@ 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 { ParamDefine } from '../../../pojo/ParamDefine'; +import { AiChatService, ChatMessage, ChatConversation, SecurityForm, DEFAULT_SECURITY_FORM } from '../../../service/ai-chat.service'; import { GeneralConfigService } from '../../../service/general-config.service'; import { ThemeService } from '../../../service/theme.service'; @@ -51,9 +52,13 @@ export class ChatComponent implements OnInit, OnDestroy { isAiProviderConfigured = false; showConfigModal = false; configLoading = false; + showSecurityFormModal = false; aiProviderConfig: ModelProviderConfig = new ModelProviderConfig(); providerOptions: ProviderOption[] = PROVIDER_OPTIONS; + securityParamDefine: ParamDefine[] = []; + securityParams: any = {}; + constructor( private aiChatService: AiChatService, private message: NzMessageService, @@ -192,6 +197,14 @@ export class ChatComponent implements OnInit, OnDestroy { if (response.code === 0 && response.data) { this.messages = response.data.messages || []; + //calculate security form for each message + for (let i = 0; i < this.messages.length; i++) { + const message = this.messages[i]; + const next = i < this.messages.length - 1 ? this.messages[i + 1].content : ''; + message.securityForm = this.calculateShowSecurityForm(message.content, next); + message.content = message.securityForm.content; + } + this.cdr.detectChanges(); this.scrollToBottom(); } else { @@ -253,6 +266,26 @@ export class ChatComponent implements OnInit, OnDestroy { }); } + /** + * calculate if the security form should be shown for the given message content + * + * @param content current message content + * @param next next message content + */ + calculateShowSecurityForm(content: string, next: string): SecurityForm { + const completeMessage: String[] = ['表单填写完成', '表單已完成', 'Formulário concluído', 'フォームが完了しました', 'Form completed']; + const complete = completeMessage.includes(next); + const regex = /```json\s*SecureForm:((?:.+\s)+)```/gm; + if (!content) return { show: false, param: '', content: content, complete: complete }; + const match = content.match(regex); + if (!match || match.length === 0) { + return { show: false, param: '', content: content, complete: complete }; + } + // @ts-ignore + const result = match[0].replace(/```json\s*SecureForm:/gm, '').replace(/```/, ''); + return { show: true, param: result, content: content.replace(regex, ''), complete: complete }; + } + /** * Send a message */ @@ -264,6 +297,7 @@ export class ChatComponent implements OnInit, OnDestroy { const userMessage: ChatMessage = { content: this.newMessage.trim(), role: 'user', + securityForm: DEFAULT_SECURITY_FORM, gmtCreate: new Date() }; @@ -282,6 +316,7 @@ export class ChatComponent implements OnInit, OnDestroy { const offlineMessage: ChatMessage = { content: this.i18nSvc.fanyi('ai.chat.offline.response'), role: 'assistant', + securityForm: DEFAULT_SECURITY_FORM, gmtCreate: new Date() }; this.messages.push(offlineMessage); @@ -296,6 +331,7 @@ export class ChatComponent implements OnInit, OnDestroy { const assistantMessage: ChatMessage = { content: '', role: 'assistant', + securityForm: { show: false, param: '', content: '', complete: false }, gmtCreate: new Date() }; this.messages.push(assistantMessage); @@ -311,7 +347,6 @@ export class ChatComponent implements OnInit, OnDestroy { // Accumulate the content for streaming effect lastMessage.content += chunk.content; lastMessage.gmtCreate = chunk.gmtCreate; - this.cdr.detectChanges(); this.scrollToBottom(); } @@ -332,6 +367,7 @@ export class ChatComponent implements OnInit, OnDestroy { const errorMessage: ChatMessage = { content: this.i18nSvc.fanyi('ai.chat.error.processing'), role: 'assistant', + securityForm: DEFAULT_SECURITY_FORM, gmtCreate: new Date() }; this.messages.push(errorMessage); @@ -343,6 +379,10 @@ export class ChatComponent implements OnInit, OnDestroy { this.isSendingMessage = false; this.cdr.detectChanges(); + const lastMessage = this.messages[this.messages.length - 1]; + lastMessage.securityForm = this.calculateShowSecurityForm(lastMessage.content, ''); + lastMessage.content = lastMessage.securityForm.content; + // Refresh current conversation to get updated data (only if not fallback) if (this.currentConversation && this.currentConversation.id !== 0) { this.aiChatService.getConversation(this.currentConversation.id).subscribe({ @@ -618,4 +658,62 @@ export class ChatComponent implements OnInit, OnDestroy { this.aiProviderConfig.model = selectedProvider.defaultModel; } } + + /** + * handle security form submit + * + */ + onSecurityFormSubmit(): void { + this.aiChatService + .saveSecurityData({ + securityData: JSON.stringify(Object.values(this.securityParams)), + conversationId: this.currentConversation?.id + }) + .subscribe( + (message: any) => { + if (message.code === 0) { + const lastMessage = this.messages[this.messages.length - 1]; + lastMessage.securityForm.complete = true; + const tmpMessage = this.newMessage; + this.newMessage = this.i18nSvc.fanyi('ai.chat.security.form.default.callback'); + this.sendMessage(); + this.newMessage = tmpMessage; + } else { + console.log('Error saving security data:'); + } + }, + (error: any) => { + console.error('Error saving security data:', error); + } + ); + this.showSecurityFormModal = false; + } + + /** + * handle security form cancel + */ + onSecurityFormCancel(): void { + this.showSecurityFormModal = false; + } + + /** + * handle open security form + * + * @param securityForm + */ + openSecurityForm(securityForm: SecurityForm): void { + this.securityParamDefine = JSON.parse(securityForm.param).privateParams.map((i: any) => { + this.securityParams[i.field] = { + // Parameter type 0: number 1: string 2: encrypted string 3: json string mapped by map + type: i.type === 'number' ? 0 : i.type === 'text' || i.type === 'string' ? 1 : i.type === 'json' ? 3 : 2, + field: i.field, + paramValue: null + }; + i.name = i.name[this.i18nSvc.defaultLang] || i.name['en-US'] || i.name; + return i; + }); + + console.log('this.securityParamDefine:', this.securityParamDefine); + this.showSecurityFormModal = true; + } } diff --git a/web-app/src/app/shared/shared.module.ts b/web-app/src/app/shared/shared.module.ts index 1c2735e285a..72971c6a861 100644 --- a/web-app/src/app/shared/shared.module.ts +++ b/web-app/src/app/shared/shared.module.ts @@ -73,8 +73,7 @@ const DIRECTIVES: Array> = [TimezonePipe, I18nElsePipe, ElapsedTimePi NzInputModule, NzIconModule.forChild(icons), NzSpinModule, - NzCodeEditorModule, - AiChatModule + NzCodeEditorModule ], declarations: [...COMPONENTS, ...DIRECTIVES, HelpMessageShowComponent], exports: [ @@ -89,8 +88,7 @@ const DIRECTIVES: Array> = [TimezonePipe, I18nElsePipe, ElapsedTimePi ...SHARED_ZORRO_MODULES, ...ThirdModules, ...COMPONENTS, - ...DIRECTIVES, - AiChatModule + ...DIRECTIVES ] }) export class SharedModule {} diff --git a/web-app/src/assets/i18n/en-US.json b/web-app/src/assets/i18n/en-US.json index 9a89c2fef0c..b4331056972 100644 --- a/web-app/src/assets/i18n/en-US.json +++ b/web-app/src/assets/i18n/en-US.json @@ -1133,5 +1133,13 @@ "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.security.form.title": "Security Form", + "ai.chat.security.form.button": "Enter private parameters", + "ai.chat.security.form.complete": "Complete", + "ai.chat.security.form.default.callback": "Form completed", + "ai.chat.config.participation.model": "AI Participation Mode", + "ai.chat.config.participation.model.help": "Trusted Mode: All data is sent to the LLM for processing. Protected Mode: Restricts some AI capabilities, such as preventing private data from being sent to the LLM for processing.", + "ai.chat.config.participation.model.protected": "Protected", + "ai.chat.config.participation.model.trusted": "Trusted" } diff --git a/web-app/src/assets/i18n/ja-JP.json b/web-app/src/assets/i18n/ja-JP.json index 2c1d697e1d4..256cd85ccc3 100644 --- a/web-app/src/assets/i18n/ja-JP.json +++ b/web-app/src/assets/i18n/ja-JP.json @@ -1093,5 +1093,13 @@ "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.security.form.title": "セキュアフォーム", + "ai.chat.security.form.button": "プライベートパラメータを入力してください", + "ai.chat.security.form.complete": "完了", + "ai.chat.security.form.default.callback": "フォームが完了しました", + "ai.chat.config.participation.model": "AI参加モード", + "ai.chat.config.participation.model.help": "信頼モード:すべてのデータが大規模モデルに送信され、処理されます。保護モード:プライベートデータが大規模モデルに送信され、処理されるのを防ぐなど、一部のAI機能を制限します。", + "ai.chat.config.participation.model.protected": "保護モード", + "ai.chat.config.participation.model.trusted": "信頼モード" } diff --git a/web-app/src/assets/i18n/pt-BR.json b/web-app/src/assets/i18n/pt-BR.json index ac2bd8f5908..7c0317aa1fc 100644 --- a/web-app/src/assets/i18n/pt-BR.json +++ b/web-app/src/assets/i18n/pt-BR.json @@ -1226,5 +1226,13 @@ "common.notify.import-progress": "Progresso da Importação", "common.notify.query-fail": "Falha na Consulta", "define.disable": "Desabilitar", - "define.enable": "Habilitar" + "define.enable": "Habilitar", + "ai.chat.security.form.title": "Formulários seguros", + "ai.chat.security.form.button": "Inserir parâmetros privados", + "ai.chat.security.form.complete": "Concluir", + "ai.chat.security.form.default.callback": "Formulário concluído", + "ai.chat.config.participation.model": "Modo de Participação da IA", + "ai.chat.config.participation.model.help": "Modo Confiável: Todos os dados são enviados para o modelo principal para processamento. Modo Protegido: Restringe algumas funcionalidades da IA, como impedir que dados privados sejam enviados para o modelo principal para processamento.", + "ai.chat.config.participation.model.protected": "Modo protegido", + "ai.chat.config.participation.model.trusted": "Modo confiável" } diff --git a/web-app/src/assets/i18n/zh-CN.json b/web-app/src/assets/i18n/zh-CN.json index ca614fefc9c..87e479d3b0e 100644 --- a/web-app/src/assets/i18n/zh-CN.json +++ b/web-app/src/assets/i18n/zh-CN.json @@ -1136,5 +1136,13 @@ "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.security.form.title": "安全表单", + "ai.chat.security.form.button": "填写私密参数", + "ai.chat.security.form.complete": "已填写", + "ai.chat.security.form.default.callback": "表单填写完成", + "ai.chat.config.participation.model": "AI参与模式", + "ai.chat.config.participation.model.help": "信任模式:所有的数据都会发送到大模型处理,受保护模式:限制一些AI能力,如私密数据不发送给大模型处理", + "ai.chat.config.participation.model.protected": "受保护模式", + "ai.chat.config.participation.model.trusted": "信任模式" } diff --git a/web-app/src/assets/i18n/zh-TW.json b/web-app/src/assets/i18n/zh-TW.json index b5f277c3820..3690ab12ab1 100644 --- a/web-app/src/assets/i18n/zh-TW.json +++ b/web-app/src/assets/i18n/zh-TW.json @@ -1107,5 +1107,13 @@ "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.security.form.title": "安全表單", + "ai.chat.security.form.button": "輸入私有參數", + "ai.chat.security.form.complete": "完成", + "ai.chat.security.form.default.callback": "表單已完成", + "ai.chat.config.participation.model": "AI參與模式", + "ai.chat.config.participation.model.help": "信任模式:所有的資料都會傳送到大模型處理,受保護模式:限制一些AI能力,如私密資料不傳送給大模型處理", + "ai.chat.config.participation.model.protected": "受保護模式", + "ai.chat.config.participation.model.trusted": "信任模式" }