feat(i18n): add backend internationalization — tool descriptions, error messages, guard rules, runtime context

This commit is contained in:
matevip 2026-04-11 17:08:43 +08:00
parent cc28f665f1
commit aa09345403
23 changed files with 726 additions and 179 deletions

View File

@ -36,6 +36,13 @@ public class AgentToolSet {
this.callbacks = List.copyOf(callbackByName.values()); this.callbacks = List.copyOf(callbackByName.values());
} }
/**
* 从预构建的 ToolCallback 列表构建工具集用于 i18n 等需要包装 callback 的场景
*/
public static AgentToolSet fromCallbacks(List<Object> toolBeans, List<ToolCallback> callbacks) {
return new AgentToolSet(toolBeans != null ? toolBeans : List.of(), callbacks);
}
/** /**
* @Tool Bean 列表和 ToolCallbackProvider 列表构建统一工具集 * @Tool Bean 列表和 ToolCallbackProvider 列表构建统一工具集
*/ */

View File

@ -24,18 +24,42 @@ public final class RuntimeContextInjector {
*/ */
public static String buildContextMessage() { public static String buildContextMessage() {
LocalDateTime now = LocalDateTime.now(ZONE); 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)"; + " " + now.format(TIME_FMT) + " (Asia/Shanghai)";
} }
/** /**
* 构建运行时上下文消息包含当前日期时间和工作目录 * 构建运行时上下文消息包含当前日期时间和工作目录
* 使用 I18nService 解析本地化消息
*/ */
public static String buildContextMessage(String workspaceBasePath) { 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()) { if (workspaceBasePath != null && !workspaceBasePath.isBlank()) {
sb.append("\n[system-context] 工作目录: ").append(workspaceBasePath) if (i18n != null) {
.append("\n你只能在此目录及其子目录内读写文件和执行命令。"); 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(); return sb.toString();
} }

View File

@ -28,25 +28,54 @@ public final class PromptLoader {
private PromptLoader() {} private PromptLoader() {}
/** /**
* 加载 prompt 文件内容 * 加载 prompt 文件内容默认语言
* *
* @param promptName 文件名不含路径前缀和 .txt 后缀例如 "graph/summarize-system" * @param promptName 文件名不含路径前缀和 .txt 后缀例如 "graph/summarize-system"
* @return 文件文本内容 * @return 文件文本内容
* @throws RuntimeException 文件不存在或读取失败时抛出不会静默返回空字符串 * @throws RuntimeException 文件不存在或读取失败时抛出不会静默返回空字符串
*/ */
public static String loadPrompt(String promptName) { public static String loadPrompt(String promptName) {
return promptCache.computeIfAbsent(promptName, name -> { return promptCache.computeIfAbsent(promptName, name -> readPromptFile(name, null));
String fileName = PROMPT_PATH_PREFIX + name + ".txt"; }
try (InputStream inputStream = PromptLoader.class.getClassLoader().getResourceAsStream(fileName)) {
if (inputStream == null) { /**
throw new RuntimeException("Prompt 文件不存在: " + fileName); * 加载指定语言的 prompt 文件内容
* <p>
* 查找顺序{@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 ignored) {}
} catch (IOException e) { }
log.error("加载 Prompt 失败!{}", e.getMessage(), e); // 回退到默认目录
throw new RuntimeException("加载 Prompt 失败: " + name, e); 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);
}
} }
/** /**

View File

@ -23,12 +23,21 @@ public class R<T> implements Serializable {
/** 数据 */ /** 数据 */
private T data; 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 <T> R<T> ok() { public static <T> R<T> ok() {
return result(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null); return result(ResultCode.SUCCESS.getCode(), resolveMsg(ResultCode.SUCCESS), null);
} }
public static <T> R<T> ok(T data) { public static <T> R<T> ok(T data) {
return result(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data); return result(ResultCode.SUCCESS.getCode(), resolveMsg(ResultCode.SUCCESS), data);
} }
public static <T> R<T> ok(String msg, T data) { public static <T> R<T> ok(String msg, T data) {
@ -36,7 +45,7 @@ public class R<T> implements Serializable {
} }
public static <T> R<T> fail() { public static <T> R<T> 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 <T> R<T> fail(String msg) { public static <T> R<T> fail(String msg) {

View File

@ -10,23 +10,38 @@ import lombok.Getter;
@Getter @Getter
public enum ResultCode { public enum ResultCode {
SUCCESS(200, "操作成功"), SUCCESS(200, "result.success"),
UNAUTHORIZED(401, "未登录或Token已过期"), UNAUTHORIZED(401, "result.unauthorized"),
FORBIDDEN(403, "没有权限"), FORBIDDEN(403, "result.forbidden"),
NOT_FOUND(404, "资源不存在"), NOT_FOUND(404, "result.not_found"),
SYSTEM_ERROR(500, "系统内部错误"), SYSTEM_ERROR(500, "result.system_error"),
PARAM_ERROR(400, "参数校验失败"), PARAM_ERROR(400, "result.param_error"),
AGENT_NOT_FOUND(1001, "Agent不存在"), AGENT_NOT_FOUND(1001, "result.agent_not_found"),
AGENT_BUSY(1002, "Agent正在执行任务请稍后"), AGENT_BUSY(1002, "result.agent_busy"),
LLM_ERROR(2001, "大模型调用失败"), LLM_ERROR(2001, "result.llm_error"),
TOOL_NOT_FOUND(3001, "工具不存在"), TOOL_NOT_FOUND(3001, "result.tool_not_found"),
CHANNEL_ERROR(4001, "渠道消息发送失败"); CHANNEL_ERROR(4001, "result.channel_error");
private final int code; 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.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;
} }
} }

View File

@ -30,7 +30,7 @@ public class GlobalExceptionHandler {
return null; return null;
} }
log.warn("Async request timeout: {} {}", request.getMethod(), request.getRequestURI()); 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) @ExceptionHandler(MateClawException.class)
@ -44,7 +44,7 @@ public class GlobalExceptionHandler {
String msg = e.getBindingResult().getFieldErrors().stream() String msg = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.findFirst() .findFirst()
.orElse("参数校验失败"); .orElse("Validation failed");
log.warn("Validation failed: {}", msg); log.warn("Validation failed: {}", msg);
return R.fail(400, msg); return R.fail(400, msg);
} }
@ -60,7 +60,7 @@ public class GlobalExceptionHandler {
return null; return null;
} }
log.error("Unexpected error", e); log.error("Unexpected error", e);
return R.fail("系统内部错误:" + e.getMessage()); return R.fail("Internal error: " + e.getMessage());
} }
/** /**

View File

@ -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);
}
}

View File

@ -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;
/**
* 国际化服务 统一的消息解析入口
* <p>
* 根据 {@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;
}
}

View File

@ -0,0 +1,38 @@
package vip.mate.i18n;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
/**
* 国际化工具回调装饰器
* <p>
* 包装原始 ToolCallback覆写 {@link #getToolDefinition()} 返回本地化描述
* 其他方法callname 全部委托给原始回调
*
* @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);
}
}

View File

@ -9,10 +9,15 @@ import org.springframework.stereotype.Component;
import vip.mate.tool.model.ToolEntity; import vip.mate.tool.model.ToolEntity;
import vip.mate.tool.repository.ToolMapper; 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 org.springframework.ai.tool.ToolCallbackProvider;
import vip.mate.agent.AgentToolSet; import vip.mate.agent.AgentToolSet;
import vip.mate.i18n.I18nService;
import vip.mate.i18n.LocaleAwareToolCallback;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -32,6 +37,7 @@ public class ToolRegistry {
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final ToolMapper toolMapper; private final ToolMapper toolMapper;
private final I18nService i18nService;
/** /**
* 获取所有已启用的工具 BeanSpring AI @Tool 注解方式 * 获取所有已启用的工具 BeanSpring AI @Tool 注解方式
@ -86,8 +92,35 @@ public class ToolRegistry {
List<Object> toolBeans = getEnabledTools(); List<Object> toolBeans = getEnabledTools();
Map<String, ToolCallbackProvider> providerBeans = applicationContext.getBeansOfType(ToolCallbackProvider.class); Map<String, ToolCallbackProvider> providerBeans = applicationContext.getBeansOfType(ToolCallbackProvider.class);
List<ToolCallbackProvider> providers = new ArrayList<>(providerBeans.values()); List<ToolCallbackProvider> providers = new ArrayList<>(providerBeans.values());
log.info("Building AgentToolSet: toolBeans={}, providers={}", toolBeans.size(), providers.size());
return AgentToolSet.from(toolBeans, providers); // 对内置工具 callback 应用 i18n 描述包装
List<ToolCallback> 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);
} }
/** /**

View File

@ -15,19 +15,19 @@ import java.time.format.DateTimeFormatter;
@Component @Component
public class DateTimeTool { 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() { public String getCurrentDateTime() {
return LocalDateTime.now(ZoneId.of("Asia/Shanghai")) return LocalDateTime.now(ZoneId.of("Asia/Shanghai"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); .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() { public String getCurrentDate() {
return LocalDateTime.now(ZoneId.of("Asia/Shanghai")) return LocalDateTime.now(ZoneId.of("Asia/Shanghai"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); .format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} }
@Tool(description = "获取当前时间,返回格式为 HH:mm:ss") @Tool(description = "Get current time in HH:mm:ss format")
public String getCurrentTime() { public String getCurrentTime() {
return LocalDateTime.now(ZoneId.of("Asia/Shanghai")) return LocalDateTime.now(ZoneId.of("Asia/Shanghai"))
.format(DateTimeFormatter.ofPattern("HH:mm:ss")); .format(DateTimeFormatter.ofPattern("HH:mm:ss"));

View File

@ -34,13 +34,12 @@ public class DelegateAgentTool {
private final AgentMapper agentMapper; private final AgentMapper agentMapper;
@Tool(description = """ @Tool(description = """
委派任务给另一个 Agent 执行实现多 Agent 协作 Delegate a task to another Agent for multi-agent collaboration. \
当前任务需要特定领域的 Agent 协助时使用此工具 Target Agent executes in an independent session and returns its final reply. \
目标 Agent 将在独立会话中执行任务返回其最终回复 Provide complete task context.""")
注意需提供完整的任务上下文目标 Agent 无法看到当前对话历史""")
public String delegateToAgent( public String delegateToAgent(
@ToolParam(description = "目标 Agent 的名称(精确匹配)") String agentName, @ToolParam(description = "Target Agent name (exact match)") String agentName,
@ToolParam(description = "要委派的任务描述,必须包含完整上下文信息") String task) { @ToolParam(description = "Task description with complete context information") String task) {
// 1. 参数校验 // 1. 参数校验
if (agentName == null || agentName.isBlank()) { 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() { public String listAvailableAgents() {
List<AgentEntity> agents = agentMapper.selectList( List<AgentEntity> agents = agentMapper.selectList(
new LambdaQueryWrapper<AgentEntity>() new LambdaQueryWrapper<AgentEntity>()

View File

@ -28,32 +28,35 @@ import java.nio.file.Paths;
*/ */
@Slf4j @Slf4j
@Component @Component
@lombok.RequiredArgsConstructor
public class EditFileTool { public class EditFileTool {
@Tool(description = "通过查找替换编辑文件内容。找到 old_text 精确匹配的文本并替换为 new_text。" private final vip.mate.i18n.I18nService i18n;
+ "返回包含 filePath、replacements替换次数的结构化 JSON 结果。"
+ "注意:需要用户审批确认。如果 old_text 在文件中出现多次,默认只替换第一处,设置 replaceAll=true 替换全部。") @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( public String edit_file(
@ToolParam(description = "文件的绝对路径或相对路径") String filePath, @ToolParam(description = "Absolute or relative file path") String filePath,
@ToolParam(description = "要查找的原始文本(精确匹配)") String oldText, @ToolParam(description = "Original text to find (exact match)") String oldText,
@ToolParam(description = "替换后的新文本") String newText, @ToolParam(description = "Replacement text") String newText,
@ToolParam(description = "是否替换所有匹配项,默认 false仅替换第一处", required = false) Boolean replaceAll) { @ToolParam(description = "Replace all occurrences, default false (first only)", required = false) Boolean replaceAll) {
JSONObject result = new JSONObject(); JSONObject result = new JSONObject();
result.set("filePath", filePath); result.set("filePath", filePath);
try { try {
if (filePath == null || filePath.isBlank()) { if (filePath == null || filePath.isBlank()) {
return errorResult(filePath, "文件路径不能为空"); return errorResult(filePath, i18n.msg("tool.edit_file.error.path_empty"));
} }
if (oldText == null || oldText.isEmpty()) { if (oldText == null || oldText.isEmpty()) {
return errorResult(filePath, "oldText 不能为空"); return errorResult(filePath, i18n.msg("tool.edit_file.error.old_text_empty"));
} }
if (newText == null) { if (newText == null) {
newText = ""; newText = "";
} }
if (oldText.equals(newText)) { if (oldText.equals(newText)) {
return errorResult(filePath, "oldText 和 newText 内容相同,无需替换"); return errorResult(filePath, i18n.msg("tool.edit_file.error.same_text"));
} }
Path path; Path path;
@ -64,13 +67,13 @@ public class EditFileTool {
} }
if (!Files.exists(path)) { if (!Files.exists(path)) {
return errorResult(filePath, "文件不存在: " + path); return errorResult(filePath, i18n.msg("tool.edit_file.error.not_found", path));
} }
if (Files.isDirectory(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)) { 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 是否存在 // 检查 oldText 是否存在
if (!content.contains(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("replacements", replacements);
result.set("replaceAll", doReplaceAll); result.set("replaceAll", doReplaceAll);
result.set("message", "编辑成功: 替换了 " + replacements + " 处匹配"); result.set("message", "Edit successful: " + replacements + " replacement(s)");
log.info("[EditFile] Edited {}: {} replacement(s)", path, replacements); log.info("[EditFile] Edited {}: {} replacement(s)", path, replacements);
} catch (Exception e) { } catch (Exception e) {
log.error("[EditFile] Failed to edit file: {}", e.getMessage(), 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); return JSONUtil.toJsonPrettyStr(result);

View File

@ -29,19 +29,16 @@ public class ImageGenerateTool {
private final SystemSettingService systemSettingService; private final SystemSettingService systemSettingService;
private final AsyncTaskService asyncTaskService; private final AsyncTaskService asyncTaskService;
@Tool(description = "图片生成工具,支持以下 action\n" @Tool(description = "Image generation tool. Supports actions: generate (default), list (show available providers), "
+ "- generate默认生成图片。提供 prompt 描述图片内容,可选 size/aspectRatio/model/count\n" + "status (check task status). Some providers are async (30s-2min), results auto-displayed in conversation.")
+ "- list列出所有可用的图片 Provider 及其支持的模型和能力\n"
+ "- status查看当前会话中正在进行的图片生成任务状态\n"
+ "部分 Provider 是异步生成30秒-2分钟完成后自动显示在对话中。")
public String image_generate( public String image_generate(
@ToolParam(description = "操作类型: generate生成图片、list列出可用 Provider、status查看任务状态默认 generate", required = false) String action, @ToolParam(description = "Action type: generate, list, status. Default: generate", required = false) String action,
@ToolParam(description = "图片内容描述尽量详细generate 时必填)", required = false) String prompt, @ToolParam(description = "Image content description, be detailed (required for generate)", required = false) String prompt,
@ToolParam(description = "图片尺寸: 1024x1024 / 1024x1792 / 1792x1024", required = false) String size, @ToolParam(description = "Image size: 1024x1024 / 1024x1792 / 1792x1024", required = false) String size,
@ToolParam(description = "画面比例: 1:1 / 16:9 / 9:16默认 1:1", required = false) String aspectRatio, @ToolParam(description = "Aspect ratio: 1:1 / 16:9 / 9:16, default 1:1", required = false) String aspectRatio,
@ToolParam(description = "生成数量1-4默认 1", required = false) Integer count, @ToolParam(description = "Generation count (1-4), default 1", required = false) Integer count,
@ToolParam(description = "指定模型名称(可选)", required = false) String model, @ToolParam(description = "Model name (optional)", required = false) String model,
@ToolParam(description = "查询指定任务 ID 的状态status 模式时使用)", required = false) String taskId @ToolParam(description = "Task ID to check status (for status action)", required = false) String taskId
) { ) {
String normalizedAction = (action == null || action.isBlank()) ? "generate" : action.trim().toLowerCase(); String normalizedAction = (action == null || action.isBlank()) ? "generate" : action.trim().toLowerCase();

View File

@ -30,8 +30,11 @@ import java.util.Set;
*/ */
@Slf4j @Slf4j
@Component @Component
@lombok.RequiredArgsConstructor
public class ReadFileTool { public class ReadFileTool {
private final vip.mate.i18n.I18nService i18n;
private static final int DEFAULT_MAX_LINES = 1000; private static final int DEFAULT_MAX_LINES = 1000;
private static final int MAX_OUTPUT_BYTES = 30 * 1024; // 30KB private static final int MAX_OUTPUT_BYTES = 30 * 1024; // 30KB
@ -44,19 +47,14 @@ public class ReadFileTool {
); );
@Tool(description = """ @Tool(description = """
读取指定文件的内容支持按行范围读取1-based Read the contents of a file. Supports line-range reading (1-based). \
返回包含 filePathtotalLinesreadLinescontent 的结构化 JSON 结果 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.""")
重要限制
- 仅支持文本文件.txt, .md, .json, .xml, .csv, .log, 源代码等
- 不支持 PDFWordExcelPowerPoint Office 文档
- 如需读取 PDF/Word 文档请使用 extract_document_text 工具
""")
public String read_file( public String read_file(
@ToolParam(description = "文件的绝对路径或相对路径") String filePath, @ToolParam(description = "Absolute or relative file path") String filePath,
@ToolParam(description = "起始行号(从 1 开始,包含),不传则从第 1 行开始", required = false) Integer startLine, @ToolParam(description = "Start line number (1-based, inclusive). Omit to start from line 1", required = false) Integer startLine,
@ToolParam(description = "结束行号(从 1 开始,包含),不传则读到末尾或达到截断上限", required = false) Integer endLine) { @ToolParam(description = "End line number (1-based, inclusive). Omit to read to EOF or truncation limit", required = false) Integer endLine) {
JSONObject result = new JSONObject(); JSONObject result = new JSONObject();
result.set("filePath", filePath); result.set("filePath", filePath);
@ -71,13 +69,13 @@ public class ReadFileTool {
// 文件存在性和类型校验 // 文件存在性和类型校验
if (!Files.exists(path)) { if (!Files.exists(path)) {
return errorResult(filePath, "文件不存在: " + path); return errorResult(filePath, i18n.msg("tool.read_file.error.not_found", path));
} }
if (Files.isDirectory(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)) { 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) { 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); start = Math.max(1, start);
end = Math.min(end, totalLines); end = Math.min(end, totalLines);
if (start > end) { if (start > end) {
return errorResult(filePath, "起始行 " + start + " 大于结束行 " + end); return errorResult(filePath, i18n.msg("tool.read_file.error.start_gt_end", start, end));
} }
// 提取指定范围的行转为 0-based // 提取指定范围的行转为 0-based
@ -147,7 +145,7 @@ public class ReadFileTool {
} catch (Exception e) { } catch (Exception e) {
log.error("[ReadFile] Failed to read file: {}", e.getMessage(), 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); return JSONUtil.toJsonPrettyStr(result);

View File

@ -33,19 +33,22 @@ import java.util.concurrent.TimeUnit;
*/ */
@Slf4j @Slf4j
@Component @Component
@lombok.RequiredArgsConstructor
public class ShellExecuteTool { public class ShellExecuteTool {
private final vip.mate.i18n.I18nService i18n;
private static final int DEFAULT_TIMEOUT_SECONDS = 60; private static final int DEFAULT_TIMEOUT_SECONDS = 60;
private static final int MAX_OUTPUT_BYTES = 10_000; private static final int MAX_OUTPUT_BYTES = 10_000;
private static final boolean IS_WINDOWS = System.getProperty("os.name", "") private static final boolean IS_WINDOWS = System.getProperty("os.name", "")
.toLowerCase(Locale.ROOT).contains("win"); .toLowerCase(Locale.ROOT).contains("win");
@Tool(description = "在本地服务器上执行 Shell 命令。用于执行系统命令、查看文件、运行脚本等操作。" @Tool(description = "Execute a shell command on the local server. For running system commands, viewing files, running scripts. "
+ "Windows 下使用 cmd.exeLinux/macOS 下使用 /bin/sh。" + "Uses cmd.exe on Windows, /bin/sh on Linux/macOS. "
+ "危险操作(如 rm -rf、格式化磁盘等会触发安全审批。返回包含 exitCode、stdout、stderr、timedOut 的结构化结果。") + "Dangerous operations trigger security approval. Returns structured result with exitCode, stdout, stderr, timedOut.")
public String execute_shell_command( public String execute_shell_command(
@ToolParam(description = "要执行的 Shell 命令") String command, @ToolParam(description = "Shell command to execute") String command,
@ToolParam(description = "超时秒数,默认 60 秒", required = false) Integer timeoutSeconds) { @ToolParam(description = "Timeout in seconds, default 60", required = false) Integer timeoutSeconds) {
int timeout = (timeoutSeconds != null && timeoutSeconds > 0) ? timeoutSeconds : DEFAULT_TIMEOUT_SECONDS; int timeout = (timeoutSeconds != null && timeoutSeconds > 0) ? timeoutSeconds : DEFAULT_TIMEOUT_SECONDS;
// 硬上限不允许超过 300 // 硬上限不允许超过 300
@ -93,7 +96,7 @@ public class ShellExecuteTool {
result.set("stdout", readFileTruncated(stdoutFile, MAX_OUTPUT_BYTES)); result.set("stdout", readFileTruncated(stdoutFile, MAX_OUTPUT_BYTES));
result.set("stderr", readFileTruncated(stderrFile, MAX_OUTPUT_BYTES)); result.set("stderr", readFileTruncated(stderrFile, MAX_OUTPUT_BYTES));
result.set("timedOut", true); result.set("timedOut", true);
result.set("message", "命令执行超时(" + timeout + "秒),已强制终止"); result.set("message", i18n.msg("tool.shell.error.timeout", timeout));
} else { } else {
int exitCode = process.exitValue(); int exitCode = process.exitValue();
String stdout = readFileTruncated(stdoutFile, MAX_OUTPUT_BYTES); String stdout = readFileTruncated(stdoutFile, MAX_OUTPUT_BYTES);
@ -110,7 +113,7 @@ public class ShellExecuteTool {
log.error("[ShellExecute] Command execution failed: {}", e.getMessage(), e); log.error("[ShellExecute] Command execution failed: {}", e.getMessage(), e);
result.set("exitCode", -1); result.set("exitCode", -1);
result.set("stdout", ""); 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("timedOut", false);
result.set("error", e.getMessage()); result.set("error", e.getMessage());
} finally { } finally {
@ -213,12 +216,12 @@ public class ShellExecuteTool {
byte[] data = is.readNBytes(maxBytes); byte[] data = is.readNBytes(maxBytes);
String content = new String(data, StandardCharsets.UTF_8); String content = new String(data, StandardCharsets.UTF_8);
if (truncated) { if (truncated) {
content += "\n... [输出已截断,超过 " + maxBytes + " 字节限制]"; content += "\n... [output truncated, exceeds " + maxBytes + " byte limit]";
} }
return content; return content;
} }
} catch (IOException e) { } catch (IOException e) {
return "[读取输出失败: " + e.getMessage() + "]"; return "[read output failed: " + e.getMessage() + "]";
} }
} }

View File

@ -21,14 +21,13 @@ public class WebSearchTool {
private final WebSearchService webSearchService; private final WebSearchService webSearchService;
@Tool(description = "在互联网上搜索最新信息。当需要查询实时新闻、最新数据或不确定的事实时使用此工具。" @Tool(description = "Search the internet for latest information. Use when querying real-time news, latest data, or uncertain facts. "
+ "支持可选参数freshness 控制时间范围day/week/month/year" + "Supports optional freshness, language, count parameters.")
+ "language 指定语言偏好zh-CN/encount 指定结果数量1-10")
public String search( public String search(
@ToolParam(description = "搜索关键词") String query, @ToolParam(description = "Search keywords") String query,
@ToolParam(description = "时间范围过滤: day (今天), week (本周), month (本月), year (今年)", required = false) String freshness, @ToolParam(description = "Time range filter: day (today), week (this week), month (this month), year (this year)", required = false) String freshness,
@ToolParam(description = "语言偏好: zh-CN (中文), en (英文)", required = false) String language, @ToolParam(description = "Language preference: zh-CN (Chinese), en (English)", required = false) String language,
@ToolParam(description = "最大结果数量: 1-10, 默认 5", required = false) Integer count @ToolParam(description = "Max results: 1-10, default 5", required = false) Integer count
) { ) {
SearchQuery searchQuery = new SearchQuery(query, freshness, language, count); SearchQuery searchQuery = new SearchQuery(query, freshness, language, count);
return webSearchService.search(searchQuery); return webSearchService.search(searchQuery);

View File

@ -28,21 +28,24 @@ import java.nio.file.Paths;
*/ */
@Slf4j @Slf4j
@Component @Component
@lombok.RequiredArgsConstructor
public class WriteFileTool { public class WriteFileTool {
@Tool(description = "将内容写入指定文件。如果文件已存在则完全覆写,不存在则创建新文件(自动创建父目录)。" private final vip.mate.i18n.I18nService i18n;
+ "返回包含 filePath、bytesWritten 的结构化 JSON 结果。"
+ "注意:此操作会覆盖已有文件内容,需要用户审批确认。") @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( public String write_file(
@ToolParam(description = "文件的绝对路径或相对路径") String filePath, @ToolParam(description = "Absolute or relative file path") String filePath,
@ToolParam(description = "要写入的文件内容") String content) { @ToolParam(description = "Content to write to the file") String content) {
JSONObject result = new JSONObject(); JSONObject result = new JSONObject();
result.set("filePath", filePath); result.set("filePath", filePath);
try { try {
if (filePath == null || filePath.isBlank()) { if (filePath == null || filePath.isBlank()) {
return errorResult(filePath, "文件路径不能为空"); return errorResult(filePath, i18n.msg("tool.write_file.error.path_empty"));
} }
if (content == null) { if (content == null) {
content = ""; content = "";
@ -57,7 +60,7 @@ public class WriteFileTool {
// 如果路径是已有目录拒绝 // 如果路径是已有目录拒绝
if (Files.isDirectory(path)) { 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("created", !existed);
result.set("overwritten", existed); result.set("overwritten", existed);
result.set("message", existed result.set("message", existed
? "文件已覆写: " + path + " (" + bytes.length + " 字节)" ? "Overwritten: " + path + " (" + bytes.length + " bytes)"
: "文件已创建: " + path + " (" + bytes.length + " 字节)"); : "Created: " + path + " (" + bytes.length + " bytes)");
log.info("[WriteFile] {} file: {} ({} bytes)", log.info("[WriteFile] {} file: {} ({} bytes)",
existed ? "Overwritten" : "Created", path, bytes.length); existed ? "Overwritten" : "Created", path, bytes.length);
} catch (Exception e) { } catch (Exception e) {
log.error("[WriteFile] Failed to write file: {}", e.getMessage(), 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); return JSONUtil.toJsonPrettyStr(result);

View File

@ -45,7 +45,7 @@ public final class WorkspacePathGuard {
// 先用 normalize 检查再尝试 toRealPath 防符号链接逃逸 // 先用 normalize 检查再尝试 toRealPath 防符号链接逃逸
if (!normalized.startsWith(root)) { if (!normalized.startsWith(root)) {
throw new IllegalArgumentException( 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; Path realRoot = root.toFile().exists() ? root.toRealPath() : root;
if (!realPath.startsWith(realRoot)) { if (!realPath.startsWith(realRoot)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"路径通过符号链接逃逸出工作区: " + realPath + ",允许的根目录: " + realRoot); "Path escapes workspace via symlink: " + realPath + ", allowed root: " + realRoot);
} }
return realPath; return realPath;
} }

View File

@ -15,6 +15,8 @@ import vip.mate.tool.guard.model.ToolGuardRuleEntity;
import vip.mate.tool.guard.repository.ToolGuardConfigMapper; import vip.mate.tool.guard.repository.ToolGuardConfigMapper;
import vip.mate.tool.guard.repository.ToolGuardRuleMapper; import vip.mate.tool.guard.repository.ToolGuardRuleMapper;
import vip.mate.i18n.I18nService;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -36,6 +38,7 @@ public class ToolGuardRuleSeedService implements ApplicationRunner {
private final ToolGuardRuleMapper ruleMapper; private final ToolGuardRuleMapper ruleMapper;
private final ToolGuardConfigMapper configMapper; private final ToolGuardConfigMapper configMapper;
private final I18nService i18n;
/** 旧类名 → 新 @Tool 方法名 */ /** 旧类名 → 新 @Tool 方法名 */
private static final Map<String, String> TOOL_NAME_RENAMES = Map.of( private static final Map<String, String> TOOL_NAME_RENAMES = Map.of(
@ -209,127 +212,132 @@ public class ToolGuardRuleSeedService implements ApplicationRunner {
List<ToolGuardRuleEntity> rules = new ArrayList<>(); List<ToolGuardRuleEntity> rules = new ArrayList<>();
// === CRITICAL Shell Rules === // === 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", 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", 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", 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", 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", 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", 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", 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", GuardSeverity.CRITICAL, GuardCategory.NETWORK_ABUSE, "BLOCK",
"execute_shell_command", "此命令可能被用于远程控制", 200)); "execute_shell_command", gf("SHELL_REVERSE_SHELL"), 200));
// === HIGH Shell Rules === // === 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", 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", GuardSeverity.HIGH, GuardCategory.CODE_EXECUTION, "NEEDS_APPROVAL",
"execute_shell_command", "先解码查看内容再执行", 150)); "execute_shell_command", gf("SHELL_OBFUSCATED_EXEC"), 150));
// === Credential Rules === // === 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", 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", 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", GuardSeverity.HIGH, GuardCategory.CREDENTIAL_EXPOSURE, "BLOCK",
null, "请勿在参数中传递私钥", 140)); null, gf("CRED_PRIVATE_KEY"), 140));
return rules; 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, private ToolGuardRuleEntity rule(String ruleId, String name, String pattern,
GuardSeverity severity, GuardCategory category, GuardSeverity severity, GuardCategory category,
String decision, String toolName, String remediation, String decision, String toolName, String remediation,

View File

@ -6,6 +6,9 @@ server:
spring: spring:
application: application:
name: mateclaw-server name: mateclaw-server
messages:
basename: messages
encoding: UTF-8
servlet: servlet:
multipart: multipart:
max-file-size: 100MB max-file-size: 100MB

View File

@ -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

View File

@ -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.