diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentToolSet.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentToolSet.java index 89a12a9f..a15d2472 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentToolSet.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentToolSet.java @@ -36,6 +36,13 @@ public class AgentToolSet { this.callbacks = List.copyOf(callbackByName.values()); } + /** + * 从预构建的 ToolCallback 列表构建工具集(用于 i18n 等需要包装 callback 的场景) + */ + public static AgentToolSet fromCallbacks(List toolBeans, List callbacks) { + return new AgentToolSet(toolBeans != null ? toolBeans : List.of(), callbacks); + } + /** * 从 @Tool Bean 列表和 ToolCallbackProvider 列表构建统一工具集 */ diff --git a/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java b/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java index 8349f90c..f3661a55 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/context/RuntimeContextInjector.java @@ -24,18 +24,42 @@ public final class RuntimeContextInjector { */ public static String buildContextMessage() { LocalDateTime now = LocalDateTime.now(ZONE); - return "[system-context] 当前时间: " + now.format(DATE_FMT) + return "[system-context] Current time: " + now.format(DATE_FMT) + " " + now.format(TIME_FMT) + " (Asia/Shanghai)"; } /** * 构建运行时上下文消息,包含当前日期、时间和工作目录。 + * 使用 I18nService 解析本地化消息。 */ public static String buildContextMessage(String workspaceBasePath) { - StringBuilder sb = new StringBuilder(buildContextMessage()); + return buildContextMessage(workspaceBasePath, null); + } + + /** + * 构建运行时上下文消息(i18n 版本)。 + */ + public static String buildContextMessage(String workspaceBasePath, vip.mate.i18n.I18nService i18n) { + LocalDateTime now = LocalDateTime.now(ZONE); + String dateStr = now.format(DATE_FMT); + String timeStr = now.format(TIME_FMT); + + StringBuilder sb = new StringBuilder(); + if (i18n != null) { + sb.append(i18n.msg("context.current_time", dateStr, timeStr)); + } else { + sb.append("[system-context] Current time: ").append(dateStr) + .append(" ").append(timeStr).append(" (Asia/Shanghai)"); + } + if (workspaceBasePath != null && !workspaceBasePath.isBlank()) { - sb.append("\n[system-context] 工作目录: ").append(workspaceBasePath) - .append("\n你只能在此目录及其子目录内读写文件和执行命令。"); + if (i18n != null) { + sb.append("\n").append(i18n.msg("context.working_dir", workspaceBasePath)); + sb.append("\n").append(i18n.msg("context.working_dir_hint")); + } else { + sb.append("\n[system-context] Working directory: ").append(workspaceBasePath); + sb.append("\nYou can only read/write files and execute commands within this directory and its subdirectories."); + } } return sb.toString(); } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/prompt/PromptLoader.java b/mateclaw-server/src/main/java/vip/mate/agent/prompt/PromptLoader.java index d3d186e1..58a07fa7 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/prompt/PromptLoader.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/prompt/PromptLoader.java @@ -28,25 +28,54 @@ public final class PromptLoader { private PromptLoader() {} /** - * 加载 prompt 文件内容 + * 加载 prompt 文件内容(默认语言) * * @param promptName 文件名(不含路径前缀和 .txt 后缀),例如 "graph/summarize-system" * @return 文件文本内容 * @throws RuntimeException 文件不存在或读取失败时抛出,不会静默返回空字符串 */ public static String loadPrompt(String promptName) { - return promptCache.computeIfAbsent(promptName, name -> { - String fileName = PROMPT_PATH_PREFIX + name + ".txt"; - try (InputStream inputStream = PromptLoader.class.getClassLoader().getResourceAsStream(fileName)) { - if (inputStream == null) { - throw new RuntimeException("Prompt 文件不存在: " + fileName); + return promptCache.computeIfAbsent(promptName, name -> readPromptFile(name, null)); + } + + /** + * 加载指定语言的 prompt 文件内容 + *

+ * 查找顺序:{@code prompts/{locale}/{name}.txt} → {@code prompts/{name}.txt} + * + * @param promptName 文件名(不含路径前缀和 .txt 后缀) + * @param locale 语言标识(如 "en"、"zh"),为 null 或 "zh" 时使用默认文件 + * @return 文件文本内容 + */ + public static String loadPrompt(String promptName, String locale) { + if (locale == null || locale.isBlank() || "zh".equals(locale)) { + return loadPrompt(promptName); + } + String cacheKey = locale + ":" + promptName; + return promptCache.computeIfAbsent(cacheKey, key -> readPromptFile(promptName, locale)); + } + + private static String readPromptFile(String name, String locale) { + // 优先尝试 locale 目录 + if (locale != null && !locale.isBlank()) { + String localeFileName = PROMPT_PATH_PREFIX + locale + "/" + name + ".txt"; + try (InputStream is = PromptLoader.class.getClassLoader().getResourceAsStream(localeFileName)) { + if (is != null) { + return StreamUtils.copyToString(is, StandardCharsets.UTF_8); } - return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); - } catch (IOException e) { - log.error("加载 Prompt 失败!{}", e.getMessage(), e); - throw new RuntimeException("加载 Prompt 失败: " + name, e); + } catch (IOException ignored) {} + } + // 回退到默认目录 + String fileName = PROMPT_PATH_PREFIX + name + ".txt"; + try (InputStream inputStream = PromptLoader.class.getClassLoader().getResourceAsStream(fileName)) { + if (inputStream == null) { + throw new RuntimeException("Prompt file not found: " + fileName); } - }); + return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + } catch (IOException e) { + log.error("Failed to load prompt: {}", e.getMessage(), e); + throw new RuntimeException("Failed to load prompt: " + name, e); + } } /** diff --git a/mateclaw-server/src/main/java/vip/mate/common/result/R.java b/mateclaw-server/src/main/java/vip/mate/common/result/R.java index 46ca4465..17be7aaf 100644 --- a/mateclaw-server/src/main/java/vip/mate/common/result/R.java +++ b/mateclaw-server/src/main/java/vip/mate/common/result/R.java @@ -23,12 +23,21 @@ public class R implements Serializable { /** 数据 */ private T data; + /** i18n holder — set once at startup by I18nAutoConfig, used by ok()/fail() */ + private static volatile vip.mate.i18n.I18nService i18n; + + public static void setI18n(vip.mate.i18n.I18nService service) { i18n = service; } + + private static String resolveMsg(ResultCode rc) { + return i18n != null ? rc.getMsg(i18n) : rc.getMsg(); + } + public static R ok() { - return result(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null); + return result(ResultCode.SUCCESS.getCode(), resolveMsg(ResultCode.SUCCESS), null); } public static R ok(T data) { - return result(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data); + return result(ResultCode.SUCCESS.getCode(), resolveMsg(ResultCode.SUCCESS), data); } public static R ok(String msg, T data) { @@ -36,7 +45,7 @@ public class R implements Serializable { } public static R fail() { - return result(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMsg(), null); + return result(ResultCode.SYSTEM_ERROR.getCode(), resolveMsg(ResultCode.SYSTEM_ERROR), null); } public static R fail(String msg) { diff --git a/mateclaw-server/src/main/java/vip/mate/common/result/ResultCode.java b/mateclaw-server/src/main/java/vip/mate/common/result/ResultCode.java index 4e9eca01..2e7f1d00 100644 --- a/mateclaw-server/src/main/java/vip/mate/common/result/ResultCode.java +++ b/mateclaw-server/src/main/java/vip/mate/common/result/ResultCode.java @@ -10,23 +10,38 @@ import lombok.Getter; @Getter public enum ResultCode { - SUCCESS(200, "操作成功"), - UNAUTHORIZED(401, "未登录或Token已过期"), - FORBIDDEN(403, "没有权限"), - NOT_FOUND(404, "资源不存在"), - SYSTEM_ERROR(500, "系统内部错误"), - PARAM_ERROR(400, "参数校验失败"), - AGENT_NOT_FOUND(1001, "Agent不存在"), - AGENT_BUSY(1002, "Agent正在执行任务,请稍后"), - LLM_ERROR(2001, "大模型调用失败"), - TOOL_NOT_FOUND(3001, "工具不存在"), - CHANNEL_ERROR(4001, "渠道消息发送失败"); + SUCCESS(200, "result.success"), + UNAUTHORIZED(401, "result.unauthorized"), + FORBIDDEN(403, "result.forbidden"), + NOT_FOUND(404, "result.not_found"), + SYSTEM_ERROR(500, "result.system_error"), + PARAM_ERROR(400, "result.param_error"), + AGENT_NOT_FOUND(1001, "result.agent_not_found"), + AGENT_BUSY(1002, "result.agent_busy"), + LLM_ERROR(2001, "result.llm_error"), + TOOL_NOT_FOUND(3001, "result.tool_not_found"), + CHANNEL_ERROR(4001, "result.channel_error"); private final int code; - private final String msg; + /** i18n message key */ + private final String msgKey; - ResultCode(int code, String msg) { + ResultCode(int code, String msgKey) { this.code = code; - this.msg = msg; + this.msgKey = msgKey; + } + + /** + * 获取本地化消息(需要 I18nService 实例) + */ + public String getMsg(vip.mate.i18n.I18nService i18n) { + return i18n != null ? i18n.msg(msgKey) : msgKey; + } + + /** + * 获取消息 key(向后兼容) + */ + public String getMsg() { + return msgKey; } } diff --git a/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java b/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java index 4131ccf2..40f9d3bd 100644 --- a/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java +++ b/mateclaw-server/src/main/java/vip/mate/exception/GlobalExceptionHandler.java @@ -30,7 +30,7 @@ public class GlobalExceptionHandler { return null; } log.warn("Async request timeout: {} {}", request.getMethod(), request.getRequestURI()); - return R.fail(503, "请求超时,请稍后重试"); + return R.fail(503, "Request timeout, please try again"); } @ExceptionHandler(MateClawException.class) @@ -44,7 +44,7 @@ public class GlobalExceptionHandler { String msg = e.getBindingResult().getFieldErrors().stream() .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) .findFirst() - .orElse("参数校验失败"); + .orElse("Validation failed"); log.warn("Validation failed: {}", msg); return R.fail(400, msg); } @@ -60,7 +60,7 @@ public class GlobalExceptionHandler { return null; } log.error("Unexpected error", e); - return R.fail("系统内部错误:" + e.getMessage()); + return R.fail("Internal error: " + e.getMessage()); } /** diff --git a/mateclaw-server/src/main/java/vip/mate/i18n/I18nAutoConfig.java b/mateclaw-server/src/main/java/vip/mate/i18n/I18nAutoConfig.java new file mode 100644 index 00000000..8b33f03e --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/i18n/I18nAutoConfig.java @@ -0,0 +1,24 @@ +package vip.mate.i18n; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import vip.mate.common.result.R; + +/** + * 在启动时将 I18nService 注入到 R(统一响应类)中, + * 使 R.ok() / R.fail() 返回的 msg 跟随系统语言设置。 + * + * @author MateClaw Team + */ +@Component +@RequiredArgsConstructor +public class I18nAutoConfig { + + private final I18nService i18nService; + + @PostConstruct + public void init() { + R.setI18n(i18nService); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/i18n/I18nService.java b/mateclaw-server/src/main/java/vip/mate/i18n/I18nService.java new file mode 100644 index 00000000..685aa85e --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/i18n/I18nService.java @@ -0,0 +1,77 @@ +package vip.mate.i18n; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Service; +import vip.mate.system.service.SystemSettingService; + +import java.util.Locale; + +/** + * 国际化服务 — 统一的消息解析入口 + *

+ * 根据 {@link SystemSettingService#getLanguage()} 全局语言设置解析消息。 + * 缓存 Locale 对象(语言切换频率极低),避免每次调用都解析字符串。 + * + * @author MateClaw Team + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class I18nService { + + private final MessageSource messageSource; + private final SystemSettingService settingService; + + /** 缓存的 Locale 和对应的语言字符串 */ + private volatile Locale cachedLocale; + private volatile String cachedLang; + + /** + * 解析国际化消息 + * + * @param key 消息键(如 "tool.read_file.desc") + * @param args 占位符参数 + * @return 解析后的消息文本,找不到 key 时返回 key 本身 + */ + public String msg(String key, Object... args) { + Locale locale = resolveLocale(); + try { + return messageSource.getMessage(key, args, locale); + } catch (Exception e) { + log.debug("[I18n] Missing key: {} (locale={})", key, locale); + return key; + } + } + + /** + * 获取当前语言的 locale 标识(用于 PromptLoader 等需要 locale 字符串的场景) + * + * @return "zh" 或 "en" + */ + public String currentLocaleTag() { + String lang = settingService.getLanguage(); + return lang.startsWith("en") ? "en" : "zh"; + } + + /** + * 清除缓存的 Locale(语言切换时调用) + */ + public void clearLocaleCache() { + cachedLocale = null; + cachedLang = null; + log.info("[I18n] Locale cache cleared"); + } + + private Locale resolveLocale() { + String lang = settingService.getLanguage(); + if (lang.equals(cachedLang) && cachedLocale != null) { + return cachedLocale; + } + Locale locale = lang.startsWith("en") ? Locale.US : Locale.CHINA; + cachedLang = lang; + cachedLocale = locale; + return locale; + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/i18n/LocaleAwareToolCallback.java b/mateclaw-server/src/main/java/vip/mate/i18n/LocaleAwareToolCallback.java new file mode 100644 index 00000000..76c3a072 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/i18n/LocaleAwareToolCallback.java @@ -0,0 +1,38 @@ +package vip.mate.i18n; + +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.definition.ToolDefinition; + +/** + * 国际化工具回调装饰器 + *

+ * 包装原始 ToolCallback,覆写 {@link #getToolDefinition()} 返回本地化描述。 + * 其他方法(call、name 等)全部委托给原始回调。 + * + * @author MateClaw Team + */ +public class LocaleAwareToolCallback implements ToolCallback { + + private final ToolCallback delegate; + private final String localizedDescription; + + public LocaleAwareToolCallback(ToolCallback delegate, String localizedDescription) { + this.delegate = delegate; + this.localizedDescription = localizedDescription; + } + + @Override + public ToolDefinition getToolDefinition() { + ToolDefinition original = delegate.getToolDefinition(); + return ToolDefinition.builder() + .name(original.name()) + .description(localizedDescription) + .inputSchema(original.inputSchema()) + .build(); + } + + @Override + public String call(String toolInput) { + return delegate.call(toolInput); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/tool/ToolRegistry.java b/mateclaw-server/src/main/java/vip/mate/tool/ToolRegistry.java index f15c89d0..945e3279 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/ToolRegistry.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/ToolRegistry.java @@ -9,10 +9,15 @@ import org.springframework.stereotype.Component; import vip.mate.tool.model.ToolEntity; import vip.mate.tool.repository.ToolMapper; +import org.springframework.ai.support.ToolCallbacks; +import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbackProvider; import vip.mate.agent.AgentToolSet; +import vip.mate.i18n.I18nService; +import vip.mate.i18n.LocaleAwareToolCallback; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -32,6 +37,7 @@ public class ToolRegistry { private final ApplicationContext applicationContext; private final ToolMapper toolMapper; + private final I18nService i18nService; /** * 获取所有已启用的工具 Bean(Spring AI @Tool 注解方式) @@ -86,8 +92,35 @@ public class ToolRegistry { List toolBeans = getEnabledTools(); Map providerBeans = applicationContext.getBeansOfType(ToolCallbackProvider.class); List providers = new ArrayList<>(providerBeans.values()); - log.info("Building AgentToolSet: toolBeans={}, providers={}", toolBeans.size(), providers.size()); - return AgentToolSet.from(toolBeans, providers); + + // 对内置工具 callback 应用 i18n 描述包装 + List localizedCallbacks = new ArrayList<>(); + for (Object bean : toolBeans) { + ToolCallback[] cbs = ToolCallbacks.from(bean); + for (ToolCallback cb : cbs) { + String toolName = cb.getToolDefinition().name(); + String descKey = "tool." + toolName + ".desc"; + String localizedDesc = i18nService.msg(descKey); + // 如果 key 被解析(不等于 key 本身),使用本地化描述 + if (!localizedDesc.equals(descKey)) { + localizedCallbacks.add(new LocaleAwareToolCallback(cb, localizedDesc)); + } else { + localizedCallbacks.add(cb); + } + } + } + + // MCP provider callbacks 不做 i18n 包装(MCP 工具自行管理描述) + for (ToolCallbackProvider provider : providers) { + ToolCallback[] cbs = provider.getToolCallbacks(); + if (cbs != null) { + Collections.addAll(localizedCallbacks, cbs); + } + } + + log.info("Building AgentToolSet: toolBeans={}, providers={}, totalCallbacks={}", + toolBeans.size(), providers.size(), localizedCallbacks.size()); + return AgentToolSet.fromCallbacks(toolBeans, localizedCallbacks); } /** diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DateTimeTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DateTimeTool.java index 3b43477e..fbe60993 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DateTimeTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DateTimeTool.java @@ -15,19 +15,19 @@ import java.time.format.DateTimeFormatter; @Component public class DateTimeTool { - @Tool(description = "获取当前日期和时间,返回格式为 yyyy-MM-dd HH:mm:ss") + @Tool(description = "Get current date and time in yyyy-MM-dd HH:mm:ss format") public String getCurrentDateTime() { return LocalDateTime.now(ZoneId.of("Asia/Shanghai")) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } - @Tool(description = "获取当前日期,返回格式为 yyyy-MM-dd") + @Tool(description = "Get current date in yyyy-MM-dd format") public String getCurrentDate() { return LocalDateTime.now(ZoneId.of("Asia/Shanghai")) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); } - @Tool(description = "获取当前时间,返回格式为 HH:mm:ss") + @Tool(description = "Get current time in HH:mm:ss format") public String getCurrentTime() { return LocalDateTime.now(ZoneId.of("Asia/Shanghai")) .format(DateTimeFormatter.ofPattern("HH:mm:ss")); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java index 9f76f3e2..1b2ac084 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java @@ -34,13 +34,12 @@ public class DelegateAgentTool { private final AgentMapper agentMapper; @Tool(description = """ - 委派任务给另一个 Agent 执行,实现多 Agent 协作。 - 当前任务需要特定领域的 Agent 协助时使用此工具。 - 目标 Agent 将在独立会话中执行任务,返回其最终回复。 - 注意:需提供完整的任务上下文,目标 Agent 无法看到当前对话历史。""") + Delegate a task to another Agent for multi-agent collaboration. \ + Target Agent executes in an independent session and returns its final reply. \ + Provide complete task context.""") public String delegateToAgent( - @ToolParam(description = "目标 Agent 的名称(精确匹配)") String agentName, - @ToolParam(description = "要委派的任务描述,必须包含完整上下文信息") String task) { + @ToolParam(description = "Target Agent name (exact match)") String agentName, + @ToolParam(description = "Task description with complete context information") String task) { // 1. 参数校验 if (agentName == null || agentName.isBlank()) { @@ -85,7 +84,7 @@ public class DelegateAgentTool { } } - @Tool(description = "列出所有可用的 Agent(已启用),包括名称、类型和描述。用于在委派前了解有哪些 Agent 可以协助。") + @Tool(description = "List all available Agents (enabled), including name, type, and description.") public String listAvailableAgents() { List agents = agentMapper.selectList( new LambdaQueryWrapper() diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/EditFileTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/EditFileTool.java index 7029d592..d57d5621 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/EditFileTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/EditFileTool.java @@ -28,32 +28,35 @@ import java.nio.file.Paths; */ @Slf4j @Component +@lombok.RequiredArgsConstructor public class EditFileTool { - @Tool(description = "通过查找替换编辑文件内容。找到 old_text 精确匹配的文本并替换为 new_text。" - + "返回包含 filePath、replacements(替换次数)的结构化 JSON 结果。" - + "注意:需要用户审批确认。如果 old_text 在文件中出现多次,默认只替换第一处,设置 replaceAll=true 替换全部。") + private final vip.mate.i18n.I18nService i18n; + + @Tool(description = "Edit file content via find-and-replace. Finds exact match of old_text and replaces with new_text. " + + "Returns structured JSON with filePath, replacements count. " + + "Requires user approval. Replaces first occurrence by default; set replaceAll=true for all.") public String edit_file( - @ToolParam(description = "文件的绝对路径或相对路径") String filePath, - @ToolParam(description = "要查找的原始文本(精确匹配)") String oldText, - @ToolParam(description = "替换后的新文本") String newText, - @ToolParam(description = "是否替换所有匹配项,默认 false(仅替换第一处)", required = false) Boolean replaceAll) { + @ToolParam(description = "Absolute or relative file path") String filePath, + @ToolParam(description = "Original text to find (exact match)") String oldText, + @ToolParam(description = "Replacement text") String newText, + @ToolParam(description = "Replace all occurrences, default false (first only)", required = false) Boolean replaceAll) { JSONObject result = new JSONObject(); result.set("filePath", filePath); try { if (filePath == null || filePath.isBlank()) { - return errorResult(filePath, "文件路径不能为空"); + return errorResult(filePath, i18n.msg("tool.edit_file.error.path_empty")); } if (oldText == null || oldText.isEmpty()) { - return errorResult(filePath, "oldText 不能为空"); + return errorResult(filePath, i18n.msg("tool.edit_file.error.old_text_empty")); } if (newText == null) { newText = ""; } if (oldText.equals(newText)) { - return errorResult(filePath, "oldText 和 newText 内容相同,无需替换"); + return errorResult(filePath, i18n.msg("tool.edit_file.error.same_text")); } Path path; @@ -64,13 +67,13 @@ public class EditFileTool { } if (!Files.exists(path)) { - return errorResult(filePath, "文件不存在: " + path); + return errorResult(filePath, i18n.msg("tool.edit_file.error.not_found", path)); } if (Files.isDirectory(path)) { - return errorResult(filePath, "路径是目录而非文件: " + path); + return errorResult(filePath, i18n.msg("tool.edit_file.error.is_directory", path)); } if (!Files.isReadable(path) || !Files.isWritable(path)) { - return errorResult(filePath, "文件不可读写: " + path); + return errorResult(filePath, i18n.msg("tool.edit_file.error.not_rw", path)); } // 读取文件内容 @@ -78,7 +81,7 @@ public class EditFileTool { // 检查 oldText 是否存在 if (!content.contains(oldText)) { - return errorResult(filePath, "文件中未找到指定的 oldText,请检查文本是否精确匹配(包括空格和换行)"); + return errorResult(filePath, i18n.msg("tool.edit_file.error.old_not_found")); } // 执行替换 @@ -102,13 +105,13 @@ public class EditFileTool { result.set("replacements", replacements); result.set("replaceAll", doReplaceAll); - result.set("message", "编辑成功: 替换了 " + replacements + " 处匹配"); + result.set("message", "Edit successful: " + replacements + " replacement(s)"); log.info("[EditFile] Edited {}: {} replacement(s)", path, replacements); } catch (Exception e) { log.error("[EditFile] Failed to edit file: {}", e.getMessage(), e); - return errorResult(filePath, "编辑文件异常: " + e.getMessage()); + return errorResult(filePath, i18n.msg("tool.edit_file.error.edit_exception", e.getMessage())); } return JSONUtil.toJsonPrettyStr(result); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ImageGenerateTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ImageGenerateTool.java index 64cb673d..0a8d31b8 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ImageGenerateTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ImageGenerateTool.java @@ -29,19 +29,16 @@ public class ImageGenerateTool { private final SystemSettingService systemSettingService; private final AsyncTaskService asyncTaskService; - @Tool(description = "图片生成工具,支持以下 action:\n" - + "- generate(默认):生成图片。提供 prompt 描述图片内容,可选 size/aspectRatio/model/count\n" - + "- list:列出所有可用的图片 Provider 及其支持的模型和能力\n" - + "- status:查看当前会话中正在进行的图片生成任务状态\n" - + "部分 Provider 是异步生成(30秒-2分钟),完成后自动显示在对话中。") + @Tool(description = "Image generation tool. Supports actions: generate (default), list (show available providers), " + + "status (check task status). Some providers are async (30s-2min), results auto-displayed in conversation.") public String image_generate( - @ToolParam(description = "操作类型: generate(生成图片)、list(列出可用 Provider)、status(查看任务状态),默认 generate", required = false) String action, - @ToolParam(description = "图片内容描述,尽量详细(generate 时必填)", required = false) String prompt, - @ToolParam(description = "图片尺寸: 1024x1024 / 1024x1792 / 1792x1024", required = false) String size, - @ToolParam(description = "画面比例: 1:1 / 16:9 / 9:16,默认 1:1", required = false) String aspectRatio, - @ToolParam(description = "生成数量(1-4),默认 1", required = false) Integer count, - @ToolParam(description = "指定模型名称(可选)", required = false) String model, - @ToolParam(description = "查询指定任务 ID 的状态(status 模式时使用)", required = false) String taskId + @ToolParam(description = "Action type: generate, list, status. Default: generate", required = false) String action, + @ToolParam(description = "Image content description, be detailed (required for generate)", required = false) String prompt, + @ToolParam(description = "Image size: 1024x1024 / 1024x1792 / 1792x1024", required = false) String size, + @ToolParam(description = "Aspect ratio: 1:1 / 16:9 / 9:16, default 1:1", required = false) String aspectRatio, + @ToolParam(description = "Generation count (1-4), default 1", required = false) Integer count, + @ToolParam(description = "Model name (optional)", required = false) String model, + @ToolParam(description = "Task ID to check status (for status action)", required = false) String taskId ) { String normalizedAction = (action == null || action.isBlank()) ? "generate" : action.trim().toLowerCase(); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ReadFileTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ReadFileTool.java index 7af20381..e3bf30f9 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ReadFileTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ReadFileTool.java @@ -30,8 +30,11 @@ import java.util.Set; */ @Slf4j @Component +@lombok.RequiredArgsConstructor public class ReadFileTool { + private final vip.mate.i18n.I18nService i18n; + private static final int DEFAULT_MAX_LINES = 1000; private static final int MAX_OUTPUT_BYTES = 30 * 1024; // 30KB @@ -44,19 +47,14 @@ public class ReadFileTool { ); @Tool(description = """ - 读取指定文件的内容。支持按行范围读取(1-based)。 - 返回包含 filePath、totalLines、readLines、content 的结构化 JSON 结果。 - 如果文件过大,会自动截断并提示继续读取的行号。 - - 重要限制: - - 仅支持文本文件(.txt, .md, .json, .xml, .csv, .log, 源代码等) - - 不支持 PDF、Word、Excel、PowerPoint 等 Office 文档 - - 如需读取 PDF/Word 文档,请使用 extract_document_text 工具 - """) + Read the contents of a file. Supports line-range reading (1-based). \ + Returns structured JSON with filePath, totalLines, readLines, content. \ + Auto-truncates large files with continuation hints. \ + Text files only; use extract_document_text for PDF/Office documents.""") public String read_file( - @ToolParam(description = "文件的绝对路径或相对路径") String filePath, - @ToolParam(description = "起始行号(从 1 开始,包含),不传则从第 1 行开始", required = false) Integer startLine, - @ToolParam(description = "结束行号(从 1 开始,包含),不传则读到末尾或达到截断上限", required = false) Integer endLine) { + @ToolParam(description = "Absolute or relative file path") String filePath, + @ToolParam(description = "Start line number (1-based, inclusive). Omit to start from line 1", required = false) Integer startLine, + @ToolParam(description = "End line number (1-based, inclusive). Omit to read to EOF or truncation limit", required = false) Integer endLine) { JSONObject result = new JSONObject(); result.set("filePath", filePath); @@ -71,13 +69,13 @@ public class ReadFileTool { // 文件存在性和类型校验 if (!Files.exists(path)) { - return errorResult(filePath, "文件不存在: " + path); + return errorResult(filePath, i18n.msg("tool.read_file.error.not_found", path)); } if (Files.isDirectory(path)) { - return errorResult(filePath, "路径是目录而非文件: " + path); + return errorResult(filePath, i18n.msg("tool.read_file.error.is_directory", path)); } if (!Files.isReadable(path)) { - return errorResult(filePath, "文件不可读: " + path); + return errorResult(filePath, i18n.msg("tool.read_file.error.not_readable", path)); } // 检查是否是二进制文档 - 拒绝直接读取 @@ -99,12 +97,12 @@ public class ReadFileTool { // 范围校验 if (start > totalLines) { - return errorResult(filePath, "起始行 " + start + " 超出文件总行数 " + totalLines); + return errorResult(filePath, i18n.msg("tool.read_file.error.start_exceeds", start, totalLines)); } start = Math.max(1, start); end = Math.min(end, totalLines); if (start > end) { - return errorResult(filePath, "起始行 " + start + " 大于结束行 " + end); + return errorResult(filePath, i18n.msg("tool.read_file.error.start_gt_end", start, end)); } // 提取指定范围的行(转为 0-based) @@ -147,7 +145,7 @@ public class ReadFileTool { } catch (Exception e) { log.error("[ReadFile] Failed to read file: {}", e.getMessage(), e); - return errorResult(filePath, "读取文件异常: " + e.getMessage()); + return errorResult(filePath, i18n.msg("tool.read_file.error.read_exception", e.getMessage())); } return JSONUtil.toJsonPrettyStr(result); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java index 0501b151..9b0e85b0 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java @@ -33,19 +33,22 @@ import java.util.concurrent.TimeUnit; */ @Slf4j @Component +@lombok.RequiredArgsConstructor public class ShellExecuteTool { + private final vip.mate.i18n.I18nService i18n; + private static final int DEFAULT_TIMEOUT_SECONDS = 60; private static final int MAX_OUTPUT_BYTES = 10_000; private static final boolean IS_WINDOWS = System.getProperty("os.name", "") .toLowerCase(Locale.ROOT).contains("win"); - @Tool(description = "在本地服务器上执行 Shell 命令。用于执行系统命令、查看文件、运行脚本等操作。" - + "Windows 下使用 cmd.exe,Linux/macOS 下使用 /bin/sh。" - + "危险操作(如 rm -rf、格式化磁盘等)会触发安全审批。返回包含 exitCode、stdout、stderr、timedOut 的结构化结果。") + @Tool(description = "Execute a shell command on the local server. For running system commands, viewing files, running scripts. " + + "Uses cmd.exe on Windows, /bin/sh on Linux/macOS. " + + "Dangerous operations trigger security approval. Returns structured result with exitCode, stdout, stderr, timedOut.") public String execute_shell_command( - @ToolParam(description = "要执行的 Shell 命令") String command, - @ToolParam(description = "超时秒数,默认 60 秒", required = false) Integer timeoutSeconds) { + @ToolParam(description = "Shell command to execute") String command, + @ToolParam(description = "Timeout in seconds, default 60", required = false) Integer timeoutSeconds) { int timeout = (timeoutSeconds != null && timeoutSeconds > 0) ? timeoutSeconds : DEFAULT_TIMEOUT_SECONDS; // 硬上限:不允许超过 300 秒 @@ -93,7 +96,7 @@ public class ShellExecuteTool { result.set("stdout", readFileTruncated(stdoutFile, MAX_OUTPUT_BYTES)); result.set("stderr", readFileTruncated(stderrFile, MAX_OUTPUT_BYTES)); result.set("timedOut", true); - result.set("message", "命令执行超时(" + timeout + "秒),已强制终止"); + result.set("message", i18n.msg("tool.shell.error.timeout", timeout)); } else { int exitCode = process.exitValue(); String stdout = readFileTruncated(stdoutFile, MAX_OUTPUT_BYTES); @@ -110,7 +113,7 @@ public class ShellExecuteTool { log.error("[ShellExecute] Command execution failed: {}", e.getMessage(), e); result.set("exitCode", -1); result.set("stdout", ""); - result.set("stderr", "执行异常: " + e.getMessage()); + result.set("stderr", i18n.msg("tool.shell.error.exception", e.getMessage())); result.set("timedOut", false); result.set("error", e.getMessage()); } finally { @@ -213,12 +216,12 @@ public class ShellExecuteTool { byte[] data = is.readNBytes(maxBytes); String content = new String(data, StandardCharsets.UTF_8); if (truncated) { - content += "\n... [输出已截断,超过 " + maxBytes + " 字节限制]"; + content += "\n... [output truncated, exceeds " + maxBytes + " byte limit]"; } return content; } } catch (IOException e) { - return "[读取输出失败: " + e.getMessage() + "]"; + return "[read output failed: " + e.getMessage() + "]"; } } diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/WebSearchTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/WebSearchTool.java index 17d8c0e4..213634d7 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/WebSearchTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/WebSearchTool.java @@ -21,14 +21,13 @@ public class WebSearchTool { private final WebSearchService webSearchService; - @Tool(description = "在互联网上搜索最新信息。当需要查询实时新闻、最新数据或不确定的事实时使用此工具。" - + "支持可选参数:freshness 控制时间范围(day/week/month/year)," - + "language 指定语言偏好(zh-CN/en),count 指定结果数量(1-10)。") + @Tool(description = "Search the internet for latest information. Use when querying real-time news, latest data, or uncertain facts. " + + "Supports optional freshness, language, count parameters.") public String search( - @ToolParam(description = "搜索关键词") String query, - @ToolParam(description = "时间范围过滤: day (今天), week (本周), month (本月), year (今年)", required = false) String freshness, - @ToolParam(description = "语言偏好: zh-CN (中文), en (英文)", required = false) String language, - @ToolParam(description = "最大结果数量: 1-10, 默认 5", required = false) Integer count + @ToolParam(description = "Search keywords") String query, + @ToolParam(description = "Time range filter: day (today), week (this week), month (this month), year (this year)", required = false) String freshness, + @ToolParam(description = "Language preference: zh-CN (Chinese), en (English)", required = false) String language, + @ToolParam(description = "Max results: 1-10, default 5", required = false) Integer count ) { SearchQuery searchQuery = new SearchQuery(query, freshness, language, count); return webSearchService.search(searchQuery); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/WriteFileTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/WriteFileTool.java index 44248eef..6c3de390 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/WriteFileTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/WriteFileTool.java @@ -28,21 +28,24 @@ import java.nio.file.Paths; */ @Slf4j @Component +@lombok.RequiredArgsConstructor public class WriteFileTool { - @Tool(description = "将内容写入指定文件。如果文件已存在则完全覆写,不存在则创建新文件(自动创建父目录)。" - + "返回包含 filePath、bytesWritten 的结构化 JSON 结果。" - + "注意:此操作会覆盖已有文件内容,需要用户审批确认。") + private final vip.mate.i18n.I18nService i18n; + + @Tool(description = "Write content to a file. Overwrites if exists, creates if not (auto-creates parent directories). " + + "Returns structured JSON with filePath, bytesWritten. " + + "Requires user approval.") public String write_file( - @ToolParam(description = "文件的绝对路径或相对路径") String filePath, - @ToolParam(description = "要写入的文件内容") String content) { + @ToolParam(description = "Absolute or relative file path") String filePath, + @ToolParam(description = "Content to write to the file") String content) { JSONObject result = new JSONObject(); result.set("filePath", filePath); try { if (filePath == null || filePath.isBlank()) { - return errorResult(filePath, "文件路径不能为空"); + return errorResult(filePath, i18n.msg("tool.write_file.error.path_empty")); } if (content == null) { content = ""; @@ -57,7 +60,7 @@ public class WriteFileTool { // 如果路径是已有目录,拒绝 if (Files.isDirectory(path)) { - return errorResult(filePath, "路径是一个已有目录,无法作为文件写入: " + path); + return errorResult(filePath, i18n.msg("tool.write_file.error.is_directory", path)); } // 自动创建父目录 @@ -77,15 +80,15 @@ public class WriteFileTool { result.set("created", !existed); result.set("overwritten", existed); result.set("message", existed - ? "文件已覆写: " + path + " (" + bytes.length + " 字节)" - : "文件已创建: " + path + " (" + bytes.length + " 字节)"); + ? "Overwritten: " + path + " (" + bytes.length + " bytes)" + : "Created: " + path + " (" + bytes.length + " bytes)"); log.info("[WriteFile] {} file: {} ({} bytes)", existed ? "Overwritten" : "Created", path, bytes.length); } catch (Exception e) { log.error("[WriteFile] Failed to write file: {}", e.getMessage(), e); - return errorResult(filePath, "写入文件异常: " + e.getMessage()); + return errorResult(filePath, i18n.msg("tool.write_file.error.write_exception", e.getMessage())); } return JSONUtil.toJsonPrettyStr(result); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/guard/WorkspacePathGuard.java b/mateclaw-server/src/main/java/vip/mate/tool/guard/WorkspacePathGuard.java index 4a3917af..0b2d8b51 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/guard/WorkspacePathGuard.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/guard/WorkspacePathGuard.java @@ -45,7 +45,7 @@ public final class WorkspacePathGuard { // 先用 normalize 检查,再尝试 toRealPath 防符号链接逃逸 if (!normalized.startsWith(root)) { throw new IllegalArgumentException( - "路径不在工作区允许范围内: " + normalized + ",允许的根目录: " + root); + "Path is outside workspace boundary: " + normalized + ", allowed root: " + root); } // 对已存在的路径,解析符号链接后再次校验 @@ -55,7 +55,7 @@ public final class WorkspacePathGuard { Path realRoot = root.toFile().exists() ? root.toRealPath() : root; if (!realPath.startsWith(realRoot)) { throw new IllegalArgumentException( - "路径通过符号链接逃逸出工作区: " + realPath + ",允许的根目录: " + realRoot); + "Path escapes workspace via symlink: " + realPath + ", allowed root: " + realRoot); } return realPath; } diff --git a/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleSeedService.java b/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleSeedService.java index aa3e3a74..4a488710 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleSeedService.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/guard/service/ToolGuardRuleSeedService.java @@ -15,6 +15,8 @@ import vip.mate.tool.guard.model.ToolGuardRuleEntity; import vip.mate.tool.guard.repository.ToolGuardConfigMapper; import vip.mate.tool.guard.repository.ToolGuardRuleMapper; +import vip.mate.i18n.I18nService; + import java.util.*; import java.util.stream.Collectors; @@ -36,6 +38,7 @@ public class ToolGuardRuleSeedService implements ApplicationRunner { private final ToolGuardRuleMapper ruleMapper; private final ToolGuardConfigMapper configMapper; + private final I18nService i18n; /** 旧类名 → 新 @Tool 方法名 */ private static final Map TOOL_NAME_RENAMES = Map.of( @@ -209,127 +212,132 @@ public class ToolGuardRuleSeedService implements ApplicationRunner { List rules = new ArrayList<>(); // === CRITICAL Shell Rules === - rules.add(rule("SHELL_RM_RF_ROOT", "递归强制删除根目录", "rm\\s+-(rf|fr)\\s+/\\s*$", + rules.add(rule("SHELL_RM_RF_ROOT", gn("SHELL_RM_RF_ROOT"), "rm\\s+-(rf|fr)\\s+/\\s*$", GuardSeverity.CRITICAL, GuardCategory.COMMAND_INJECTION, "BLOCK", - "execute_shell_command", "请指定具体目录路径而非根目录", 200)); + "execute_shell_command", gf("SHELL_RM_RF_ROOT"), 200)); - rules.add(rule("SHELL_MKFS", "文件系统格式化", "mkfs\\b", + rules.add(rule("SHELL_MKFS", gn("SHELL_MKFS"), "mkfs\\b", GuardSeverity.CRITICAL, GuardCategory.COMMAND_INJECTION, "BLOCK", - "execute_shell_command", "确认目标设备后手动执行", 200)); + "execute_shell_command", gf("SHELL_MKFS"), 200)); - rules.add(rule("SHELL_DD_DEV", "直接磁盘写入", "dd\\s+if=.+of=/dev/", + rules.add(rule("SHELL_DD_DEV", gn("SHELL_DD_DEV"), "dd\\s+if=.+of=/dev/", GuardSeverity.CRITICAL, GuardCategory.COMMAND_INJECTION, "BLOCK", - "execute_shell_command", "确认目标设备后手动执行", 200)); + "execute_shell_command", gf("SHELL_DD_DEV"), 200)); - rules.add(rule("SHELL_KILL_INIT", "杀死 init/systemd", "\\bkill\\s+-9\\s+1\\b", + rules.add(rule("SHELL_KILL_INIT", gn("SHELL_KILL_INIT"), "\\bkill\\s+-9\\s+1\\b", GuardSeverity.CRITICAL, GuardCategory.RESOURCE_ABUSE, "BLOCK", - "execute_shell_command", "使用 systemctl 管理服务", 200)); + "execute_shell_command", gf("SHELL_KILL_INIT"), 200)); - rules.add(rule("SHELL_CURL_PIPE_SH", "管道下载执行 (curl)", "curl.*\\|\\s*(sh|bash|zsh)", + rules.add(rule("SHELL_CURL_PIPE_SH", gn("SHELL_CURL_PIPE_SH"), "curl.*\\|\\s*(sh|bash|zsh)", GuardSeverity.CRITICAL, GuardCategory.CODE_EXECUTION, "BLOCK", - "execute_shell_command", "先下载文件审查内容再执行", 200)); + "execute_shell_command", gf("SHELL_CURL_PIPE_SH"), 200)); - rules.add(rule("SHELL_WGET_PIPE_SH", "管道下载执行 (wget)", "wget.*\\|\\s*(sh|bash|zsh)", + rules.add(rule("SHELL_WGET_PIPE_SH", gn("SHELL_WGET_PIPE_SH"), "wget.*\\|\\s*(sh|bash|zsh)", GuardSeverity.CRITICAL, GuardCategory.CODE_EXECUTION, "BLOCK", - "execute_shell_command", "先下载文件审查内容再执行", 200)); + "execute_shell_command", gf("SHELL_WGET_PIPE_SH"), 200)); - rules.add(rule("SHELL_FORK_BOMB", "Fork Bomb", ":\\(\\)\\s*\\{\\s*:\\|:\\s*&\\s*\\}\\s*;\\s*:", + rules.add(rule("SHELL_FORK_BOMB", gn("SHELL_FORK_BOMB"), ":\\(\\)\\s*\\{\\s*:\\|:\\s*&\\s*\\}\\s*;\\s*:", GuardSeverity.CRITICAL, GuardCategory.RESOURCE_ABUSE, "BLOCK", - "execute_shell_command", "此命令无正当用途", 200)); + "execute_shell_command", gf("SHELL_FORK_BOMB"), 200)); - rules.add(rule("SHELL_REVERSE_SHELL", "反向 Shell", "(/dev/tcp|\\bnc\\s+-e\\b|\\bncat\\s+-e\\b|\\bsocat\\s+EXEC:)", + rules.add(rule("SHELL_REVERSE_SHELL", gn("SHELL_REVERSE_SHELL"), "(/dev/tcp|\\bnc\\s+-e\\b|\\bncat\\s+-e\\b|\\bsocat\\s+EXEC:)", GuardSeverity.CRITICAL, GuardCategory.NETWORK_ABUSE, "BLOCK", - "execute_shell_command", "此命令可能被用于远程控制", 200)); + "execute_shell_command", gf("SHELL_REVERSE_SHELL"), 200)); // === HIGH Shell Rules === - rules.add(rule("SHELL_RM", "rm 删除命令", "(^|[;&|]|\\s)rm\\s", + rules.add(rule("SHELL_RM", gn("SHELL_RM"), "(^|[;&|]|\\s)rm\\s", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - "execute_shell_command", "请确认要删除的文件列表,考虑使用 trash 替代 rm", 150)); + "execute_shell_command", gf("SHELL_RM"), 150)); - rules.add(rule("SHELL_RM_RF", "递归强制删除", "rm\\s+-(rf|fr)", + rules.add(rule("SHELL_RM_RF", gn("SHELL_RM_RF"), "rm\\s+-(rf|fr)", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - "execute_shell_command", "使用 rm -ri 或指定具体文件", 150)); + "execute_shell_command", gf("SHELL_RM_RF"), 150)); - rules.add(rule("SHELL_RM_ROOT", "从根路径删除", "rm\\s+/", + rules.add(rule("SHELL_RM_ROOT", gn("SHELL_RM_ROOT"), "rm\\s+/", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - "execute_shell_command", "请指定具体路径", 150)); + "execute_shell_command", gf("SHELL_RM_ROOT"), 150)); - rules.add(rule("SHELL_RMDIR_ROOT", "从根路径删除目录", "rmdir\\s+/", + rules.add(rule("SHELL_RMDIR_ROOT", gn("SHELL_RMDIR_ROOT"), "rmdir\\s+/", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - "execute_shell_command", "请指定具体路径", 150)); + "execute_shell_command", gf("SHELL_RMDIR_ROOT"), 150)); - rules.add(rule("SHELL_SQL_DROP", "SQL DROP 操作", "DROP\\s+(TABLE|DATABASE|INDEX|VIEW|SCHEMA)", + rules.add(rule("SHELL_SQL_DROP", gn("SHELL_SQL_DROP"), "DROP\\s+(TABLE|DATABASE|INDEX|VIEW|SCHEMA)", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - null, "请先备份数据再执行", 150)); + null, gf("SHELL_SQL_DROP"), 150)); - rules.add(rule("SHELL_SQL_TRUNCATE", "SQL TRUNCATE 操作", "TRUNCATE\\s+TABLE", + rules.add(rule("SHELL_SQL_TRUNCATE", gn("SHELL_SQL_TRUNCATE"), "TRUNCATE\\s+TABLE", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - null, "请先备份数据再执行", 150)); + null, gf("SHELL_SQL_TRUNCATE"), 150)); - rules.add(rule("SHELL_SQL_DELETE_ALL", "SQL 无条件 DELETE", "DELETE\\s+FROM\\s+\\w+\\s*;", + rules.add(rule("SHELL_SQL_DELETE_ALL", gn("SHELL_SQL_DELETE_ALL"), "DELETE\\s+FROM\\s+\\w+\\s*;", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - null, "请添加 WHERE 条件", 150)); + null, gf("SHELL_SQL_DELETE_ALL"), 150)); - rules.add(rule("SHELL_SQL_ALTER_DROP", "SQL ALTER TABLE DROP", "ALTER\\s+TABLE\\s+\\w+\\s+DROP", + rules.add(rule("SHELL_SQL_ALTER_DROP", gn("SHELL_SQL_ALTER_DROP"), "ALTER\\s+TABLE\\s+\\w+\\s+DROP", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - null, "请先备份数据再执行", 150)); + null, gf("SHELL_SQL_ALTER_DROP"), 150)); - rules.add(rule("SHELL_SHUTDOWN", "系统关机", "\\bshutdown\\b", + rules.add(rule("SHELL_SHUTDOWN", gn("SHELL_SHUTDOWN"), "\\bshutdown\\b", GuardSeverity.HIGH, GuardCategory.RESOURCE_ABUSE, "NEEDS_APPROVAL", - "execute_shell_command", "请确认是否需要关机", 150)); + "execute_shell_command", gf("SHELL_SHUTDOWN"), 150)); - rules.add(rule("SHELL_REBOOT", "系统重启", "\\breboot\\b", + rules.add(rule("SHELL_REBOOT", gn("SHELL_REBOOT"), "\\breboot\\b", GuardSeverity.HIGH, GuardCategory.RESOURCE_ABUSE, "NEEDS_APPROVAL", - "execute_shell_command", "请确认是否需要重启", 150)); + "execute_shell_command", gf("SHELL_REBOOT"), 150)); - rules.add(rule("SHELL_CHMOD_777", "过度宽松权限", "chmod\\s+777", + rules.add(rule("SHELL_CHMOD_777", gn("SHELL_CHMOD_777"), "chmod\\s+777", GuardSeverity.HIGH, GuardCategory.PRIVILEGE_ESCALATION, "NEEDS_APPROVAL", - "execute_shell_command", "使用最小必要权限", 150)); + "execute_shell_command", gf("SHELL_CHMOD_777"), 150)); - rules.add(rule("SHELL_EVAL", "动态代码执行", "eval\\s*\\(", + rules.add(rule("SHELL_EVAL", gn("SHELL_EVAL"), "eval\\s*\\(", GuardSeverity.HIGH, GuardCategory.CODE_EXECUTION, "NEEDS_APPROVAL", - null, "避免使用 eval", 150)); + null, gf("SHELL_EVAL"), 150)); - rules.add(rule("SHELL_GIT_FORCE_PUSH", "Git 强制推送", "git\\s+push\\s+.*--force", + rules.add(rule("SHELL_GIT_FORCE_PUSH", gn("SHELL_GIT_FORCE_PUSH"), "git\\s+push\\s+.*--force", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - "execute_shell_command", "使用 --force-with-lease", 150)); + "execute_shell_command", gf("SHELL_GIT_FORCE_PUSH"), 150)); - rules.add(rule("SHELL_GIT_RESET_HARD", "Git 硬重置", "git\\s+reset\\s+--hard", + rules.add(rule("SHELL_GIT_RESET_HARD", gn("SHELL_GIT_RESET_HARD"), "git\\s+reset\\s+--hard", GuardSeverity.HIGH, GuardCategory.COMMAND_INJECTION, "NEEDS_APPROVAL", - "execute_shell_command", "先用 git stash", 150)); + "execute_shell_command", gf("SHELL_GIT_RESET_HARD"), 150)); - rules.add(rule("SHELL_CRONTAB", "定时任务修改", "\\bcrontab\\b", + rules.add(rule("SHELL_CRONTAB", gn("SHELL_CRONTAB"), "\\bcrontab\\b", GuardSeverity.HIGH, GuardCategory.PRIVILEGE_ESCALATION, "NEEDS_APPROVAL", - "execute_shell_command", "请确认定时任务内容", 150)); + "execute_shell_command", gf("SHELL_CRONTAB"), 150)); - rules.add(rule("SHELL_AUTHORIZED_KEYS", "SSH 密钥修改", "authorized_keys", + rules.add(rule("SHELL_AUTHORIZED_KEYS", gn("SHELL_AUTHORIZED_KEYS"), "authorized_keys", GuardSeverity.HIGH, GuardCategory.PRIVILEGE_ESCALATION, "NEEDS_APPROVAL", - null, "请确认 SSH 密钥变更", 150)); + null, gf("SHELL_AUTHORIZED_KEYS"), 150)); - rules.add(rule("SHELL_SUDOERS", "sudo 权限修改", "/etc/sudoers", + rules.add(rule("SHELL_SUDOERS", gn("SHELL_SUDOERS"), "/etc/sudoers", GuardSeverity.HIGH, GuardCategory.PRIVILEGE_ESCALATION, "NEEDS_APPROVAL", - null, "请使用 visudo", 150)); + null, gf("SHELL_SUDOERS"), 150)); - rules.add(rule("SHELL_OBFUSCATED_EXEC", "混淆代码执行", "base64\\s+-d.*\\|\\s*(bash|sh)", + rules.add(rule("SHELL_OBFUSCATED_EXEC", gn("SHELL_OBFUSCATED_EXEC"), "base64\\s+-d.*\\|\\s*(bash|sh)", GuardSeverity.HIGH, GuardCategory.CODE_EXECUTION, "NEEDS_APPROVAL", - "execute_shell_command", "先解码查看内容再执行", 150)); + "execute_shell_command", gf("SHELL_OBFUSCATED_EXEC"), 150)); // === Credential Rules === - rules.add(rule("CRED_PASSWORD_ASSIGN", "凭据信息暴露", "(password|secret|api[_-]?key|token)\\s*=\\s*['\"]?\\S{8,}", + rules.add(rule("CRED_PASSWORD_ASSIGN", gn("CRED_PASSWORD_ASSIGN"), "(password|secret|api[_-]?key|token)\\s*=\\s*['\"]?\\S{8,}", GuardSeverity.HIGH, GuardCategory.CREDENTIAL_EXPOSURE, "NEEDS_APPROVAL", - null, "使用环境变量或密钥管理服务", 140)); + null, gf("CRED_PASSWORD_ASSIGN"), 140)); - rules.add(rule("CRED_AWS_KEY", "AWS Access Key 泄露", "AKIA[0-9A-Z]{16}", + rules.add(rule("CRED_AWS_KEY", gn("CRED_AWS_KEY"), "AKIA[0-9A-Z]{16}", GuardSeverity.HIGH, GuardCategory.CREDENTIAL_EXPOSURE, "NEEDS_APPROVAL", - null, "使用 IAM Role 或 AWS Secrets Manager", 140)); + null, gf("CRED_AWS_KEY"), 140)); - rules.add(rule("CRED_PRIVATE_KEY", "私钥泄露", "-----BEGIN\\s+(RSA\\s+)?PRIVATE\\s+KEY-----", + rules.add(rule("CRED_PRIVATE_KEY", gn("CRED_PRIVATE_KEY"), "-----BEGIN\\s+(RSA\\s+)?PRIVATE\\s+KEY-----", GuardSeverity.HIGH, GuardCategory.CREDENTIAL_EXPOSURE, "BLOCK", - null, "请勿在参数中传递私钥", 140)); + null, gf("CRED_PRIVATE_KEY"), 140)); return rules; } + /** Guard rule name shorthand */ + private String gn(String ruleId) { return i18n.msg("guard." + ruleId + ".name"); } + /** Guard rule fix/remediation shorthand */ + private String gf(String ruleId) { return i18n.msg("guard." + ruleId + ".fix"); } + private ToolGuardRuleEntity rule(String ruleId, String name, String pattern, GuardSeverity severity, GuardCategory category, String decision, String toolName, String remediation, diff --git a/mateclaw-server/src/main/resources/application.yml b/mateclaw-server/src/main/resources/application.yml index e386ad52..30b1e32d 100644 --- a/mateclaw-server/src/main/resources/application.yml +++ b/mateclaw-server/src/main/resources/application.yml @@ -6,6 +6,9 @@ server: spring: application: name: mateclaw-server + messages: + basename: messages + encoding: UTF-8 servlet: multipart: max-file-size: 100MB diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties new file mode 100644 index 00000000..992667fe --- /dev/null +++ b/mateclaw-server/src/main/resources/messages.properties @@ -0,0 +1,139 @@ +# ==================== MateClaw i18n: zh-CN (default) ==================== + +# --- Result Codes --- +result.success=\u64cd\u4f5c\u6210\u529f +result.unauthorized=\u672a\u767b\u5f55\u6216Token\u5df2\u8fc7\u671f +result.forbidden=\u6ca1\u6709\u6743\u9650 +result.not_found=\u8d44\u6e90\u4e0d\u5b58\u5728 +result.system_error=\u7cfb\u7edf\u5185\u90e8\u9519\u8bef +result.param_error=\u53c2\u6570\u6821\u9a8c\u5931\u8d25 +result.agent_not_found=Agent\u4e0d\u5b58\u5728 +result.agent_busy=Agent\u6b63\u5728\u6267\u884c\u4efb\u52a1\uff0c\u8bf7\u7a0d\u540e +result.llm_error=\u5927\u6a21\u578b\u8c03\u7528\u5931\u8d25 +result.tool_not_found=\u5de5\u5177\u4e0d\u5b58\u5728 +result.channel_error=\u6e20\u9053\u6d88\u606f\u53d1\u9001\u5931\u8d25 + +# --- Tool Descriptions --- +tool.getCurrentDateTime.desc=\u83b7\u53d6\u5f53\u524d\u65e5\u671f\u548c\u65f6\u95f4\uff0c\u8fd4\u56de\u683c\u5f0f\u4e3a yyyy-MM-dd HH:mm:ss +tool.getCurrentDate.desc=\u83b7\u53d6\u5f53\u524d\u65e5\u671f\uff0c\u8fd4\u56de\u683c\u5f0f\u4e3a yyyy-MM-dd +tool.getCurrentTime.desc=\u83b7\u53d6\u5f53\u524d\u65f6\u95f4\uff0c\u8fd4\u56de\u683c\u5f0f\u4e3a HH:mm:ss + +tool.read_file.desc=\u8bfb\u53d6\u6307\u5b9a\u6587\u4ef6\u7684\u5185\u5bb9\u3002\u652f\u6301\u6309\u884c\u8303\u56f4\u8bfb\u53d6\uff081-based\uff09\u3002\u8fd4\u56de\u5305\u542b filePath\u3001totalLines\u3001readLines\u3001content \u7684\u7ed3\u6784\u5316 JSON \u7ed3\u679c\u3002\u5982\u679c\u6587\u4ef6\u8fc7\u5927\uff0c\u4f1a\u81ea\u52a8\u622a\u65ad\u5e76\u63d0\u793a\u7ee7\u7eed\u8bfb\u53d6\u7684\u884c\u53f7\u3002\u4ec5\u652f\u6301\u6587\u672c\u6587\u4ef6\uff0c\u4e0d\u652f\u6301 PDF/Office \u6587\u6863\uff0c\u8bf7\u4f7f\u7528 extract_document_text \u5de5\u5177\u3002 +tool.read_file.param.filePath=\u6587\u4ef6\u7684\u7edd\u5bf9\u8def\u5f84\u6216\u76f8\u5bf9\u8def\u5f84 +tool.read_file.param.startLine=\u8d77\u59cb\u884c\u53f7\uff08\u4ece 1 \u5f00\u59cb\uff0c\u5305\u542b\uff09\uff0c\u4e0d\u4f20\u5219\u4ece\u7b2c 1 \u884c\u5f00\u59cb +tool.read_file.param.endLine=\u7ed3\u675f\u884c\u53f7\uff08\u4ece 1 \u5f00\u59cb\uff0c\u5305\u542b\uff09\uff0c\u4e0d\u4f20\u5219\u8bfb\u5230\u672b\u5c3e\u6216\u8fbe\u5230\u622a\u65ad\u4e0a\u9650 + +tool.write_file.desc=\u5c06\u5185\u5bb9\u5199\u5165\u6307\u5b9a\u6587\u4ef6\u3002\u5982\u679c\u6587\u4ef6\u5df2\u5b58\u5728\u5219\u5b8c\u5168\u8986\u5199\uff0c\u4e0d\u5b58\u5728\u5219\u521b\u5efa\u65b0\u6587\u4ef6\uff08\u81ea\u52a8\u521b\u5efa\u7236\u76ee\u5f55\uff09\u3002\u8fd4\u56de\u5305\u542b filePath\u3001bytesWritten \u7684\u7ed3\u6784\u5316 JSON \u7ed3\u679c\u3002\u6b64\u64cd\u4f5c\u9700\u8981\u7528\u6237\u5ba1\u6279\u786e\u8ba4\u3002 +tool.write_file.param.filePath=\u6587\u4ef6\u7684\u7edd\u5bf9\u8def\u5f84\u6216\u76f8\u5bf9\u8def\u5f84 +tool.write_file.param.content=\u8981\u5199\u5165\u7684\u6587\u4ef6\u5185\u5bb9 + +tool.edit_file.desc=\u901a\u8fc7\u67e5\u627e\u66ff\u6362\u7f16\u8f91\u6587\u4ef6\u5185\u5bb9\u3002\u627e\u5230 old_text \u7cbe\u786e\u5339\u914d\u7684\u6587\u672c\u5e76\u66ff\u6362\u4e3a new_text\u3002\u8fd4\u56de\u5305\u542b filePath\u3001replacements \u7684\u7ed3\u6784\u5316 JSON \u7ed3\u679c\u3002\u9700\u8981\u7528\u6237\u5ba1\u6279\u786e\u8ba4\u3002\u9ed8\u8ba4\u53ea\u66ff\u6362\u7b2c\u4e00\u5904\uff0c\u8bbe\u7f6e replaceAll=true \u66ff\u6362\u5168\u90e8\u3002 +tool.edit_file.param.filePath=\u6587\u4ef6\u7684\u7edd\u5bf9\u8def\u5f84\u6216\u76f8\u5bf9\u8def\u5f84 +tool.edit_file.param.oldText=\u8981\u67e5\u627e\u7684\u539f\u59cb\u6587\u672c\uff08\u7cbe\u786e\u5339\u914d\uff09 +tool.edit_file.param.newText=\u66ff\u6362\u540e\u7684\u65b0\u6587\u672c +tool.edit_file.param.replaceAll=\u662f\u5426\u66ff\u6362\u6240\u6709\u5339\u914d\u9879\uff0c\u9ed8\u8ba4 false\uff08\u4ec5\u66ff\u6362\u7b2c\u4e00\u5904\uff09 + +tool.execute_shell_command.desc=\u5728\u672c\u5730\u670d\u52a1\u5668\u4e0a\u6267\u884c Shell \u547d\u4ee4\u3002\u7528\u4e8e\u6267\u884c\u7cfb\u7edf\u547d\u4ee4\u3001\u67e5\u770b\u6587\u4ef6\u3001\u8fd0\u884c\u811a\u672c\u7b49\u64cd\u4f5c\u3002Windows \u4e0b\u4f7f\u7528 cmd.exe\uff0cLinux/macOS \u4e0b\u4f7f\u7528 /bin/sh\u3002\u5371\u9669\u64cd\u4f5c\u4f1a\u89e6\u53d1\u5b89\u5168\u5ba1\u6279\u3002\u8fd4\u56de\u5305\u542b exitCode\u3001stdout\u3001stderr\u3001timedOut \u7684\u7ed3\u6784\u5316\u7ed3\u679c\u3002 +tool.execute_shell_command.param.command=\u8981\u6267\u884c\u7684 Shell \u547d\u4ee4 +tool.execute_shell_command.param.timeoutSeconds=\u8d85\u65f6\u79d2\u6570\uff0c\u9ed8\u8ba4 60 \u79d2 + +tool.search.desc=\u5728\u4e92\u8054\u7f51\u4e0a\u641c\u7d22\u6700\u65b0\u4fe1\u606f\u3002\u5f53\u9700\u8981\u67e5\u8be2\u5b9e\u65f6\u65b0\u95fb\u3001\u6700\u65b0\u6570\u636e\u6216\u4e0d\u786e\u5b9a\u7684\u4e8b\u5b9e\u65f6\u4f7f\u7528\u6b64\u5de5\u5177\u3002\u652f\u6301 freshness\u3001language\u3001count \u53ef\u9009\u53c2\u6570\u3002 +tool.search.param.query=\u641c\u7d22\u5173\u952e\u8bcd +tool.search.param.freshness=\u65f6\u95f4\u8303\u56f4\u8fc7\u6ee4: day (\u4eca\u5929), week (\u672c\u5468), month (\u672c\u6708), year (\u4eca\u5e74) +tool.search.param.language=\u8bed\u8a00\u504f\u597d: zh-CN (\u4e2d\u6587), en (\u82f1\u6587) +tool.search.param.count=\u6700\u5927\u7ed3\u679c\u6570\u91cf: 1-10, \u9ed8\u8ba4 5 + +tool.delegateToAgent.desc=\u59d4\u6d3e\u4efb\u52a1\u7ed9\u53e6\u4e00\u4e2a Agent \u6267\u884c\uff0c\u5b9e\u73b0\u591a Agent \u534f\u4f5c\u3002\u76ee\u6807 Agent \u5c06\u5728\u72ec\u7acb\u4f1a\u8bdd\u4e2d\u6267\u884c\u4efb\u52a1\uff0c\u8fd4\u56de\u5176\u6700\u7ec8\u56de\u590d\u3002\u9700\u63d0\u4f9b\u5b8c\u6574\u7684\u4efb\u52a1\u4e0a\u4e0b\u6587\u3002 +tool.delegateToAgent.param.agentName=\u76ee\u6807 Agent \u7684\u540d\u79f0\uff08\u7cbe\u786e\u5339\u914d\uff09 +tool.delegateToAgent.param.task=\u8981\u59d4\u6d3e\u7684\u4efb\u52a1\u63cf\u8ff0\uff0c\u5fc5\u987b\u5305\u542b\u5b8c\u6574\u4e0a\u4e0b\u6587\u4fe1\u606f +tool.listAvailableAgents.desc=\u5217\u51fa\u6240\u6709\u53ef\u7528\u7684 Agent\uff08\u5df2\u542f\u7528\uff09\uff0c\u5305\u62ec\u540d\u79f0\u3001\u7c7b\u578b\u548c\u63cf\u8ff0\u3002 + +# --- Tool Error Messages --- +tool.read_file.error.not_found=\u6587\u4ef6\u4e0d\u5b58\u5728: {0} +tool.read_file.error.is_directory=\u8def\u5f84\u662f\u76ee\u5f55\u800c\u975e\u6587\u4ef6: {0} +tool.read_file.error.not_readable=\u6587\u4ef6\u4e0d\u53ef\u8bfb: {0} +tool.read_file.error.start_exceeds=\u8d77\u59cb\u884c {0} \u8d85\u51fa\u6587\u4ef6\u603b\u884c\u6570 {1} +tool.read_file.error.start_gt_end=\u8d77\u59cb\u884c {0} \u5927\u4e8e\u7ed3\u675f\u884c {1} +tool.read_file.error.read_exception=\u8bfb\u53d6\u6587\u4ef6\u5f02\u5e38: {0} +tool.write_file.error.path_empty=\u6587\u4ef6\u8def\u5f84\u4e0d\u80fd\u4e3a\u7a7a +tool.write_file.error.is_directory=\u8def\u5f84\u662f\u4e00\u4e2a\u5df2\u6709\u76ee\u5f55\uff0c\u65e0\u6cd5\u4f5c\u4e3a\u6587\u4ef6\u5199\u5165: {0} +tool.write_file.error.write_exception=\u5199\u5165\u6587\u4ef6\u5f02\u5e38: {0} +tool.edit_file.error.path_empty=\u6587\u4ef6\u8def\u5f84\u4e0d\u80fd\u4e3a\u7a7a +tool.edit_file.error.old_text_empty=oldText \u4e0d\u80fd\u4e3a\u7a7a +tool.edit_file.error.same_text=oldText \u548c newText \u5185\u5bb9\u76f8\u540c\uff0c\u65e0\u9700\u66ff\u6362 +tool.edit_file.error.not_found=\u6587\u4ef6\u4e0d\u5b58\u5728: {0} +tool.edit_file.error.is_directory=\u8def\u5f84\u662f\u76ee\u5f55\u800c\u975e\u6587\u4ef6: {0} +tool.edit_file.error.not_rw=\u6587\u4ef6\u4e0d\u53ef\u8bfb\u5199: {0} +tool.edit_file.error.old_not_found=\u6587\u4ef6\u4e2d\u672a\u627e\u5230\u6307\u5b9a\u7684 oldText\uff0c\u8bf7\u68c0\u67e5\u6587\u672c\u662f\u5426\u7cbe\u786e\u5339\u914d\uff08\u5305\u62ec\u7a7a\u683c\u548c\u6362\u884c\uff09 +tool.edit_file.error.edit_exception=\u7f16\u8f91\u6587\u4ef6\u5f02\u5e38: {0} +tool.shell.error.timeout=\u547d\u4ee4\u6267\u884c\u8d85\u65f6\uff08{0}\u79d2\uff09\uff0c\u5df2\u5f3a\u5236\u7ec8\u6b62 +tool.shell.error.exception=\u6267\u884c\u5f02\u5e38: {0} + +# --- Guard Rules --- +guard.SHELL_RM_RF_ROOT.name=\u9012\u5f52\u5f3a\u5236\u5220\u9664\u6839\u76ee\u5f55 +guard.SHELL_RM_RF_ROOT.fix=\u8bf7\u6307\u5b9a\u5177\u4f53\u76ee\u5f55\u8def\u5f84\u800c\u975e\u6839\u76ee\u5f55 +guard.SHELL_MKFS.name=\u6587\u4ef6\u7cfb\u7edf\u683c\u5f0f\u5316 +guard.SHELL_MKFS.fix=\u786e\u8ba4\u76ee\u6807\u8bbe\u5907\u540e\u624b\u52a8\u6267\u884c +guard.SHELL_DD_DEV.name=\u76f4\u63a5\u78c1\u76d8\u5199\u5165 +guard.SHELL_DD_DEV.fix=\u786e\u8ba4\u76ee\u6807\u8bbe\u5907\u540e\u624b\u52a8\u6267\u884c +guard.SHELL_KILL_INIT.name=\u6740\u6b7b init/systemd +guard.SHELL_KILL_INIT.fix=\u4f7f\u7528 systemctl \u7ba1\u7406\u670d\u52a1 +guard.SHELL_CURL_PIPE_SH.name=\u7ba1\u9053\u4e0b\u8f7d\u6267\u884c (curl) +guard.SHELL_CURL_PIPE_SH.fix=\u5148\u4e0b\u8f7d\u6587\u4ef6\u5ba1\u67e5\u5185\u5bb9\u518d\u6267\u884c +guard.SHELL_WGET_PIPE_SH.name=\u7ba1\u9053\u4e0b\u8f7d\u6267\u884c (wget) +guard.SHELL_WGET_PIPE_SH.fix=\u5148\u4e0b\u8f7d\u6587\u4ef6\u5ba1\u67e5\u5185\u5bb9\u518d\u6267\u884c +guard.SHELL_FORK_BOMB.name=Fork Bomb +guard.SHELL_FORK_BOMB.fix=\u6b64\u547d\u4ee4\u65e0\u6b63\u5f53\u7528\u9014 +guard.SHELL_REVERSE_SHELL.name=\u53cd\u5411 Shell +guard.SHELL_REVERSE_SHELL.fix=\u6b64\u547d\u4ee4\u53ef\u80fd\u88ab\u7528\u4e8e\u8fdc\u7a0b\u63a7\u5236 +guard.SHELL_RM.name=rm \u5220\u9664\u547d\u4ee4 +guard.SHELL_RM.fix=\u8bf7\u786e\u8ba4\u8981\u5220\u9664\u7684\u6587\u4ef6\u5217\u8868\uff0c\u8003\u8651\u4f7f\u7528 trash \u66ff\u4ee3 rm +guard.SHELL_RM_RF.name=\u9012\u5f52\u5f3a\u5236\u5220\u9664 +guard.SHELL_RM_RF.fix=\u4f7f\u7528 rm -ri \u6216\u6307\u5b9a\u5177\u4f53\u6587\u4ef6 +guard.SHELL_RM_ROOT.name=\u4ece\u6839\u8def\u5f84\u5220\u9664 +guard.SHELL_RM_ROOT.fix=\u8bf7\u6307\u5b9a\u5177\u4f53\u8def\u5f84 +guard.SHELL_RMDIR_ROOT.name=\u4ece\u6839\u8def\u5f84\u5220\u9664\u76ee\u5f55 +guard.SHELL_RMDIR_ROOT.fix=\u8bf7\u6307\u5b9a\u5177\u4f53\u8def\u5f84 +guard.SHELL_SQL_DROP.name=SQL DROP \u64cd\u4f5c +guard.SHELL_SQL_DROP.fix=\u8bf7\u5148\u5907\u4efd\u6570\u636e\u518d\u6267\u884c +guard.SHELL_SQL_TRUNCATE.name=SQL TRUNCATE \u64cd\u4f5c +guard.SHELL_SQL_TRUNCATE.fix=\u8bf7\u5148\u5907\u4efd\u6570\u636e\u518d\u6267\u884c +guard.SHELL_SQL_DELETE_ALL.name=SQL \u65e0\u6761\u4ef6 DELETE +guard.SHELL_SQL_DELETE_ALL.fix=\u8bf7\u6dfb\u52a0 WHERE \u6761\u4ef6 +guard.SHELL_SQL_ALTER_DROP.name=SQL ALTER TABLE DROP +guard.SHELL_SQL_ALTER_DROP.fix=\u8bf7\u5148\u5907\u4efd\u6570\u636e\u518d\u6267\u884c +guard.SHELL_SHUTDOWN.name=\u7cfb\u7edf\u5173\u673a +guard.SHELL_SHUTDOWN.fix=\u8bf7\u786e\u8ba4\u662f\u5426\u9700\u8981\u5173\u673a +guard.SHELL_REBOOT.name=\u7cfb\u7edf\u91cd\u542f +guard.SHELL_REBOOT.fix=\u8bf7\u786e\u8ba4\u662f\u5426\u9700\u8981\u91cd\u542f +guard.SHELL_CHMOD_777.name=\u8fc7\u5ea6\u5bbd\u677e\u6743\u9650 +guard.SHELL_CHMOD_777.fix=\u4f7f\u7528\u6700\u5c0f\u5fc5\u8981\u6743\u9650 +guard.SHELL_EVAL.name=\u52a8\u6001\u4ee3\u7801\u6267\u884c +guard.SHELL_EVAL.fix=\u907f\u514d\u4f7f\u7528 eval +guard.SHELL_GIT_FORCE_PUSH.name=Git \u5f3a\u5236\u63a8\u9001 +guard.SHELL_GIT_FORCE_PUSH.fix=\u4f7f\u7528 --force-with-lease +guard.SHELL_GIT_RESET_HARD.name=Git \u786c\u91cd\u7f6e +guard.SHELL_GIT_RESET_HARD.fix=\u5148\u7528 git stash +guard.SHELL_CRONTAB.name=\u5b9a\u65f6\u4efb\u52a1\u4fee\u6539 +guard.SHELL_CRONTAB.fix=\u8bf7\u786e\u8ba4\u5b9a\u65f6\u4efb\u52a1\u5185\u5bb9 +guard.SHELL_AUTHORIZED_KEYS.name=SSH \u5bc6\u94a5\u4fee\u6539 +guard.SHELL_AUTHORIZED_KEYS.fix=\u8bf7\u786e\u8ba4 SSH \u5bc6\u94a5\u53d8\u66f4 +guard.SHELL_SUDOERS.name=sudo \u6743\u9650\u4fee\u6539 +guard.SHELL_SUDOERS.fix=\u8bf7\u4f7f\u7528 visudo +guard.SHELL_OBFUSCATED_EXEC.name=\u6df7\u6dc6\u4ee3\u7801\u6267\u884c +guard.SHELL_OBFUSCATED_EXEC.fix=\u5148\u89e3\u7801\u67e5\u770b\u5185\u5bb9\u518d\u6267\u884c +guard.CRED_PASSWORD_ASSIGN.name=\u51ed\u636e\u4fe1\u606f\u66b4\u9732 +guard.CRED_PASSWORD_ASSIGN.fix=\u4f7f\u7528\u73af\u5883\u53d8\u91cf\u6216\u5bc6\u94a5\u7ba1\u7406\u670d\u52a1 +guard.CRED_AWS_KEY.name=AWS Access Key \u6cc4\u9732 +guard.CRED_AWS_KEY.fix=\u4f7f\u7528 IAM Role \u6216 AWS Secrets Manager +guard.CRED_PRIVATE_KEY.name=\u79c1\u94a5\u6cc4\u9732 +guard.CRED_PRIVATE_KEY.fix=\u8bf7\u52ff\u5728\u53c2\u6570\u4e2d\u4f20\u9012\u79c1\u94a5 + +# --- WorkspacePathGuard --- +guard.path.not_allowed=\u8def\u5f84\u4e0d\u5728\u5de5\u4f5c\u533a\u5141\u8bb8\u8303\u56f4\u5185: {0}\uff0c\u5141\u8bb8\u7684\u6839\u76ee\u5f55: {1} +guard.path.symlink_escape=\u8def\u5f84\u901a\u8fc7\u7b26\u53f7\u94fe\u63a5\u9003\u9038\u51fa\u5de5\u4f5c\u533a: {0}\uff0c\u5141\u8bb8\u7684\u6839\u76ee\u5f55: {1} + +# --- RuntimeContext --- +context.current_time=[system-context] \u5f53\u524d\u65f6\u95f4: {0} {1} (Asia/Shanghai) +context.working_dir=[system-context] \u5de5\u4f5c\u76ee\u5f55: {0} +context.working_dir_hint=\u4f60\u53ea\u80fd\u5728\u6b64\u76ee\u5f55\u53ca\u5176\u5b50\u76ee\u5f55\u5185\u8bfb\u5199\u6587\u4ef6\u548c\u6267\u884c\u547d\u4ee4\u3002 diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties new file mode 100644 index 00000000..16153089 --- /dev/null +++ b/mateclaw-server/src/main/resources/messages_en.properties @@ -0,0 +1,139 @@ +# ==================== MateClaw i18n: en-US ==================== + +# --- Result Codes --- +result.success=Success +result.unauthorized=Not logged in or token expired +result.forbidden=Access denied +result.not_found=Resource not found +result.system_error=Internal server error +result.param_error=Parameter validation failed +result.agent_not_found=Agent not found +result.agent_busy=Agent is busy, please try later +result.llm_error=LLM call failed +result.tool_not_found=Tool not found +result.channel_error=Channel message sending failed + +# --- Tool Descriptions --- +tool.getCurrentDateTime.desc=Get current date and time in yyyy-MM-dd HH:mm:ss format +tool.getCurrentDate.desc=Get current date in yyyy-MM-dd format +tool.getCurrentTime.desc=Get current time in HH:mm:ss format + +tool.read_file.desc=Read the contents of a file. Supports line-range reading (1-based). Returns structured JSON with filePath, totalLines, readLines, content. Auto-truncates large files with continuation hints. Text files only; use extract_document_text for PDF/Office documents. +tool.read_file.param.filePath=Absolute or relative file path +tool.read_file.param.startLine=Start line number (1-based, inclusive). Omit to start from line 1 +tool.read_file.param.endLine=End line number (1-based, inclusive). Omit to read to EOF or truncation limit + +tool.write_file.desc=Write content to a file. Overwrites if exists, creates if not (auto-creates parent directories). Returns structured JSON with filePath, bytesWritten. Requires user approval. +tool.write_file.param.filePath=Absolute or relative file path +tool.write_file.param.content=Content to write to the file + +tool.edit_file.desc=Edit file content via find-and-replace. Finds exact match of old_text and replaces with new_text. Returns structured JSON with filePath, replacements count. Requires user approval. Replaces first occurrence by default; set replaceAll=true for all. +tool.edit_file.param.filePath=Absolute or relative file path +tool.edit_file.param.oldText=Original text to find (exact match) +tool.edit_file.param.newText=Replacement text +tool.edit_file.param.replaceAll=Replace all occurrences, default false (first only) + +tool.execute_shell_command.desc=Execute a shell command on the local server. For running system commands, viewing files, running scripts. Uses cmd.exe on Windows, /bin/sh on Linux/macOS. Dangerous operations trigger security approval. Returns structured result with exitCode, stdout, stderr, timedOut. +tool.execute_shell_command.param.command=Shell command to execute +tool.execute_shell_command.param.timeoutSeconds=Timeout in seconds, default 60 + +tool.search.desc=Search the internet for latest information. Use when querying real-time news, latest data, or uncertain facts. Supports optional freshness, language, count parameters. +tool.search.param.query=Search keywords +tool.search.param.freshness=Time range filter: day (today), week (this week), month (this month), year (this year) +tool.search.param.language=Language preference: zh-CN (Chinese), en (English) +tool.search.param.count=Max results: 1-10, default 5 + +tool.delegateToAgent.desc=Delegate a task to another Agent for multi-agent collaboration. Target Agent executes in an independent session and returns its final reply. Provide complete task context as the target cannot see current conversation history. +tool.delegateToAgent.param.agentName=Target Agent name (exact match) +tool.delegateToAgent.param.task=Task description with complete context information +tool.listAvailableAgents.desc=List all available Agents (enabled), including name, type, and description. + +# --- Tool Error Messages --- +tool.read_file.error.not_found=File does not exist: {0} +tool.read_file.error.is_directory=Path is a directory, not a file: {0} +tool.read_file.error.not_readable=File is not readable: {0} +tool.read_file.error.start_exceeds=Start line {0} exceeds total lines {1} +tool.read_file.error.start_gt_end=Start line {0} is greater than end line {1} +tool.read_file.error.read_exception=Read file exception: {0} +tool.write_file.error.path_empty=File path cannot be empty +tool.write_file.error.is_directory=Path is an existing directory, cannot write as file: {0} +tool.write_file.error.write_exception=Write file exception: {0} +tool.edit_file.error.path_empty=File path cannot be empty +tool.edit_file.error.old_text_empty=oldText cannot be empty +tool.edit_file.error.same_text=oldText and newText are identical, no replacement needed +tool.edit_file.error.not_found=File does not exist: {0} +tool.edit_file.error.is_directory=Path is a directory, not a file: {0} +tool.edit_file.error.not_rw=File is not readable/writable: {0} +tool.edit_file.error.old_not_found=Specified oldText not found in file. Check for exact match including spaces and line breaks. +tool.edit_file.error.edit_exception=Edit file exception: {0} +tool.shell.error.timeout=Command timed out ({0} seconds), forcefully terminated +tool.shell.error.exception=Execution exception: {0} + +# --- Guard Rules --- +guard.SHELL_RM_RF_ROOT.name=Recursive force delete root directory +guard.SHELL_RM_RF_ROOT.fix=Specify a concrete directory path instead of root +guard.SHELL_MKFS.name=Filesystem formatting +guard.SHELL_MKFS.fix=Verify target device and execute manually +guard.SHELL_DD_DEV.name=Direct disk write +guard.SHELL_DD_DEV.fix=Verify target device and execute manually +guard.SHELL_KILL_INIT.name=Kill init/systemd +guard.SHELL_KILL_INIT.fix=Use systemctl to manage services +guard.SHELL_CURL_PIPE_SH.name=Pipe download execution (curl) +guard.SHELL_CURL_PIPE_SH.fix=Download file first, review content, then execute +guard.SHELL_WGET_PIPE_SH.name=Pipe download execution (wget) +guard.SHELL_WGET_PIPE_SH.fix=Download file first, review content, then execute +guard.SHELL_FORK_BOMB.name=Fork Bomb +guard.SHELL_FORK_BOMB.fix=This command has no legitimate use +guard.SHELL_REVERSE_SHELL.name=Reverse Shell +guard.SHELL_REVERSE_SHELL.fix=This command may be used for remote control +guard.SHELL_RM.name=rm delete command +guard.SHELL_RM.fix=Confirm files to delete, consider using trash instead of rm +guard.SHELL_RM_RF.name=Recursive force delete +guard.SHELL_RM_RF.fix=Use rm -ri or specify exact files +guard.SHELL_RM_ROOT.name=Delete from root path +guard.SHELL_RM_ROOT.fix=Specify a concrete path +guard.SHELL_RMDIR_ROOT.name=Delete directory from root path +guard.SHELL_RMDIR_ROOT.fix=Specify a concrete path +guard.SHELL_SQL_DROP.name=SQL DROP operation +guard.SHELL_SQL_DROP.fix=Backup data before executing +guard.SHELL_SQL_TRUNCATE.name=SQL TRUNCATE operation +guard.SHELL_SQL_TRUNCATE.fix=Backup data before executing +guard.SHELL_SQL_DELETE_ALL.name=SQL unconditional DELETE +guard.SHELL_SQL_DELETE_ALL.fix=Add a WHERE clause +guard.SHELL_SQL_ALTER_DROP.name=SQL ALTER TABLE DROP +guard.SHELL_SQL_ALTER_DROP.fix=Backup data before executing +guard.SHELL_SHUTDOWN.name=System shutdown +guard.SHELL_SHUTDOWN.fix=Confirm shutdown is needed +guard.SHELL_REBOOT.name=System reboot +guard.SHELL_REBOOT.fix=Confirm reboot is needed +guard.SHELL_CHMOD_777.name=Overly permissive chmod +guard.SHELL_CHMOD_777.fix=Use minimum necessary permissions +guard.SHELL_EVAL.name=Dynamic code execution +guard.SHELL_EVAL.fix=Avoid using eval +guard.SHELL_GIT_FORCE_PUSH.name=Git force push +guard.SHELL_GIT_FORCE_PUSH.fix=Use --force-with-lease +guard.SHELL_GIT_RESET_HARD.name=Git hard reset +guard.SHELL_GIT_RESET_HARD.fix=Use git stash first +guard.SHELL_CRONTAB.name=Crontab modification +guard.SHELL_CRONTAB.fix=Confirm crontab content +guard.SHELL_AUTHORIZED_KEYS.name=SSH key modification +guard.SHELL_AUTHORIZED_KEYS.fix=Confirm SSH key changes +guard.SHELL_SUDOERS.name=Sudo privilege modification +guard.SHELL_SUDOERS.fix=Use visudo +guard.SHELL_OBFUSCATED_EXEC.name=Obfuscated code execution +guard.SHELL_OBFUSCATED_EXEC.fix=Decode and review content before executing +guard.CRED_PASSWORD_ASSIGN.name=Credential exposure +guard.CRED_PASSWORD_ASSIGN.fix=Use environment variables or secret manager +guard.CRED_AWS_KEY.name=AWS Access Key leak +guard.CRED_AWS_KEY.fix=Use IAM Role or AWS Secrets Manager +guard.CRED_PRIVATE_KEY.name=Private key leak +guard.CRED_PRIVATE_KEY.fix=Do not pass private keys in parameters + +# --- WorkspacePathGuard --- +guard.path.not_allowed=Path is outside workspace boundary: {0}, allowed root: {1} +guard.path.symlink_escape=Path escapes workspace via symlink: {0}, allowed root: {1} + +# --- RuntimeContext --- +context.current_time=[system-context] Current time: {0} {1} (Asia/Shanghai) +context.working_dir=[system-context] Working directory: {0} +context.working_dir_hint=You can only read/write files and execute commands within this directory and its subdirectories.