mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
feat(i18n): add backend internationalization — tool descriptions, error messages, guard rules, runtime context
This commit is contained in:
parent
cc28f665f1
commit
aa09345403
@ -36,6 +36,13 @@ public class AgentToolSet {
|
||||
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 列表构建统一工具集
|
||||
*/
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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 文件内容
|
||||
* <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 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -23,12 +23,21 @@ public class R<T> 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 <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) {
|
||||
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) {
|
||||
@ -36,7 +45,7 @@ public class R<T> implements Serializable {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
77
mateclaw-server/src/main/java/vip/mate/i18n/I18nService.java
Normal file
77
mateclaw-server/src/main/java/vip/mate/i18n/I18nService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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()} 返回本地化描述。
|
||||
* 其他方法(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);
|
||||
}
|
||||
}
|
||||
@ -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<Object> toolBeans = getEnabledTools();
|
||||
Map<String, ToolCallbackProvider> providerBeans = applicationContext.getBeansOfType(ToolCallbackProvider.class);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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<AgentEntity> agents = agentMapper.selectList(
|
||||
new LambdaQueryWrapper<AgentEntity>()
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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() + "]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<String, String> TOOL_NAME_RENAMES = Map.of(
|
||||
@ -209,127 +212,132 @@ public class ToolGuardRuleSeedService implements ApplicationRunner {
|
||||
List<ToolGuardRuleEntity> 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,
|
||||
|
||||
@ -6,6 +6,9 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: mateclaw-server
|
||||
messages:
|
||||
basename: messages
|
||||
encoding: UTF-8
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
|
||||
139
mateclaw-server/src/main/resources/messages.properties
Normal file
139
mateclaw-server/src/main/resources/messages.properties
Normal 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
|
||||
139
mateclaw-server/src/main/resources/messages_en.properties
Normal file
139
mateclaw-server/src/main/resources/messages_en.properties
Normal 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.
|
||||
Loading…
Reference in New Issue
Block a user