feat: BashTool sandbox, ConfigTool persistence, ToolSearchTool
- BashTool: command blacklist (fork bombs, rm -rf /, etc) + risky command warning log. Blocked commands return clear error message. - ConfigTool: JSON file persistence (~/.claude-code/config.json), new 'list' action, loads from file on first access. - ToolSearchTool: search ToolRegistry by name/description keywords, returns matching tools with descriptions (total: 23 tools). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -109,7 +109,8 @@ public class AppConfig {
|
|||||||
// P2: 配置工具
|
// P2: 配置工具
|
||||||
new ConfigTool(),
|
new ConfigTool(),
|
||||||
// P2: 实用工具
|
// P2: 实用工具
|
||||||
new SleepTool()
|
new SleepTool(),
|
||||||
|
new ToolSearchTool()
|
||||||
);
|
);
|
||||||
|
|
||||||
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
||||||
@@ -119,6 +120,9 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将 ToolRegistry 注入 ToolContext,供 ToolSearchTool 使用
|
||||||
|
toolContext.set("TOOL_REGISTRY", registry);
|
||||||
|
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.io.BufferedReader;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +29,35 @@ public class BashTool implements Tool {
|
|||||||
/** 默认超时(秒) */
|
/** 默认超时(秒) */
|
||||||
private static final int DEFAULT_TIMEOUT = 120;
|
private static final int DEFAULT_TIMEOUT = 120;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 危险命令黑名单 —— 这些命令前缀需要特别警告。
|
||||||
|
* 对应 TS 版 shouldUseSandbox.ts 的 containsExcludedCommand。
|
||||||
|
* 注意:这不是安全边界,而是用户友好的提示机制。
|
||||||
|
*/
|
||||||
|
private static final Set<String> DANGEROUS_COMMANDS = Set.of(
|
||||||
|
"rm -rf /", "rm -rf /*", "rm -rf ~",
|
||||||
|
"mkfs", "dd if=",
|
||||||
|
":(){:|:&};:", // fork bomb
|
||||||
|
"chmod -R 777 /",
|
||||||
|
"git push --force", "git push -f",
|
||||||
|
"git reset --hard",
|
||||||
|
"shutdown", "reboot", "halt",
|
||||||
|
"format c:", "del /f /s /q c:\\"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要用户确认的命令前缀 —— 风险较高但不是完全禁止的操作。
|
||||||
|
*/
|
||||||
|
private static final Set<String> WARN_COMMAND_PREFIXES = Set.of(
|
||||||
|
"rm -rf", "rm -r",
|
||||||
|
"git push --force", "git push -f",
|
||||||
|
"git reset --hard",
|
||||||
|
"git rebase",
|
||||||
|
"drop database", "drop table",
|
||||||
|
"truncate table",
|
||||||
|
"sudo rm", "sudo dd"
|
||||||
|
);
|
||||||
|
|
||||||
/** 检测到的 shell 类型 */
|
/** 检测到的 shell 类型 */
|
||||||
public enum ShellType {
|
public enum ShellType {
|
||||||
POWERSHELL("PowerShell", "pwsh", "-NoProfile", "-Command"),
|
POWERSHELL("PowerShell", "pwsh", "-NoProfile", "-Command"),
|
||||||
@@ -126,6 +156,24 @@ public class BashTool implements Tool {
|
|||||||
: DEFAULT_TIMEOUT;
|
: DEFAULT_TIMEOUT;
|
||||||
Path workDir = context.getWorkDir();
|
Path workDir = context.getWorkDir();
|
||||||
|
|
||||||
|
// Sandbox check: block absolutely dangerous commands
|
||||||
|
String cmdLower = command.toLowerCase().trim();
|
||||||
|
for (String dangerous : DANGEROUS_COMMANDS) {
|
||||||
|
if (cmdLower.equals(dangerous) || cmdLower.startsWith(dangerous)) {
|
||||||
|
return "⛔ BLOCKED: This command is potentially destructive and has been blocked.\n"
|
||||||
|
+ "Command: " + command + "\n"
|
||||||
|
+ "If you really need to run this, please ask the user to execute it manually.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sandbox warning: flag risky commands
|
||||||
|
for (String prefix : WARN_COMMAND_PREFIXES) {
|
||||||
|
if (cmdLower.startsWith(prefix)) {
|
||||||
|
log.warn("⚠️ Risky command detected: {}", command);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProcessBuilder pb = buildProcess(command);
|
ProcessBuilder pb = buildProcess(command);
|
||||||
pb.directory(workDir.toFile());
|
pb.directory(workDir.toFile());
|
||||||
|
|||||||
@@ -2,35 +2,46 @@ package com.claudecode.tool.impl;
|
|||||||
|
|
||||||
import com.claudecode.tool.Tool;
|
import com.claudecode.tool.Tool;
|
||||||
import com.claudecode.tool.ToolContext;
|
import com.claudecode.tool.ToolContext;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config 工具 —— 获取或设置配置值。
|
* Config 工具 —— 获取或设置配置值,持久化到 JSON 文件。
|
||||||
* <p>
|
* <p>
|
||||||
* 属于 P2 优先级的辅助工具。支持两种存储后端:
|
* 属于 P2 优先级的辅助工具。存储后端优先级:
|
||||||
* </p>
|
* </p>
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li>首选从 ToolContext 中的配置映射(键 "CONFIG_STORE")读写</li>
|
* <li>ToolContext 中的内存缓存(CONFIG_STORE)—— 快速读写</li>
|
||||||
* <li>回退到 {@link System#getProperty} / {@link System#setProperty}</li>
|
* <li>JSON 文件持久化(~/.claude-code/config.json)—— 跨会话保持</li>
|
||||||
|
* <li>回退到 {@link System#getProperty} —— 兜底读取</li>
|
||||||
* </ol>
|
* </ol>
|
||||||
*
|
*
|
||||||
* <h3>参数</h3>
|
* <h3>参数</h3>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li><b>action</b>(必填)—— "get" 或 "set"</li>
|
* <li><b>action</b>(必填)—— "get"、"set" 或 "list"</li>
|
||||||
* <li><b>key</b>(必填)—— 配置键名</li>
|
* <li><b>key</b>(get/set 时必填)—— 配置键名</li>
|
||||||
* <li><b>value</b>(可选,set 时必填)—— 配置值</li>
|
* <li><b>value</b>(set 时必填)—— 配置值</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
|
||||||
* <h3>返回</h3>
|
|
||||||
* <p>JSON 格式:get 返回当前值,set 返回确认信息。</p>
|
|
||||||
*/
|
*/
|
||||||
public class ConfigTool implements Tool {
|
public class ConfigTool implements Tool {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ConfigTool.class);
|
||||||
|
|
||||||
/** ToolContext 中配置存储的键名 */
|
/** ToolContext 中配置存储的键名 */
|
||||||
private static final String CONFIG_STORE_KEY = "CONFIG_STORE";
|
private static final String CONFIG_STORE_KEY = "CONFIG_STORE";
|
||||||
|
|
||||||
|
/** 配置文件路径 */
|
||||||
|
private static final Path CONFIG_FILE = Path.of(
|
||||||
|
System.getProperty("user.home"), ".claude-code", "config.json");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String name() {
|
public String name() {
|
||||||
return "Config";
|
return "Config";
|
||||||
@@ -39,8 +50,8 @@ public class ConfigTool implements Tool {
|
|||||||
@Override
|
@Override
|
||||||
public String description() {
|
public String description() {
|
||||||
return """
|
return """
|
||||||
Get or set configuration values. Use when the user requests configuration changes, \
|
Get, set, or list configuration values. Configuration persists across sessions \
|
||||||
asks about current settings, or when adjusting a setting would benefit them.
|
in ~/.claude-code/config.json.
|
||||||
|
|
||||||
Available settings include:
|
Available settings include:
|
||||||
- language: Preferred response language (e.g., "zh-CN", "en")
|
- language: Preferred response language (e.g., "zh-CN", "en")
|
||||||
@@ -50,7 +61,7 @@ public class ConfigTool implements Tool {
|
|||||||
- timeout: Default command timeout in seconds
|
- timeout: Default command timeout in seconds
|
||||||
- permissions: Permission mode (ask/auto/deny)
|
- permissions: Permission mode (ask/auto/deny)
|
||||||
|
|
||||||
Use action 'get' to read a setting value, 'set' to change it.""";
|
Use action 'get' to read, 'set' to change, 'list' to see all settings.""";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -61,26 +72,22 @@ public class ConfigTool implements Tool {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"action": {
|
"action": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Action type: get or set",
|
"description": "Action type: get, set, or list",
|
||||||
"enum": ["get", "set"]
|
"enum": ["get", "set", "list"]
|
||||||
},
|
},
|
||||||
"key": {
|
"key": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Configuration key name"
|
"description": "Configuration key name (required for get/set)"
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Configuration value (required for set operation)"
|
"description": "Configuration value (required for set operation)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["action", "key"]
|
"required": ["action"]
|
||||||
}""";
|
}""";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Config 工具不是纯只读的(set 操作会修改状态),
|
|
||||||
* 但出于安全考虑仍标记为 false。
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReadOnly() {
|
public boolean isReadOnly() {
|
||||||
return false;
|
return false;
|
||||||
@@ -88,33 +95,32 @@ public class ConfigTool implements Tool {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String execute(Map<String, Object> input, ToolContext context) {
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
// 解析必填参数: action
|
|
||||||
String action = (String) input.get("action");
|
String action = (String) input.get("action");
|
||||||
if (action == null || action.isBlank()) {
|
if (action == null || action.isBlank()) {
|
||||||
return errorJson("Parameter 'action' is required, valid values: get, set");
|
return errorJson("Parameter 'action' is required, valid values: get, set, list");
|
||||||
}
|
}
|
||||||
action = action.trim().toLowerCase();
|
action = action.trim().toLowerCase();
|
||||||
|
|
||||||
// 解析必填参数: key
|
// 获取或初始化配置存储(从文件加载)
|
||||||
String key = (String) input.get("key");
|
ConcurrentHashMap<String, String> configStore = getOrInitStore(context);
|
||||||
if (key == null || key.isBlank()) {
|
|
||||||
return errorJson("Parameter 'key' is required and cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取或初始化配置存储
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
ConcurrentHashMap<String, String> configStore =
|
|
||||||
context.getOrDefault(CONFIG_STORE_KEY, null);
|
|
||||||
|
|
||||||
if (configStore == null) {
|
|
||||||
configStore = new ConcurrentHashMap<>();
|
|
||||||
context.set(CONFIG_STORE_KEY, configStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (action) {
|
return switch (action) {
|
||||||
case "get" -> executeGet(key, configStore);
|
case "get" -> {
|
||||||
case "set" -> executeSet(key, input, configStore);
|
String key = (String) input.get("key");
|
||||||
default -> errorJson("Invalid action value: '" + action + "'. Valid values: get, set");
|
if (key == null || key.isBlank()) {
|
||||||
|
yield errorJson("'get' action requires 'key' parameter");
|
||||||
|
}
|
||||||
|
yield executeGet(key, configStore);
|
||||||
|
}
|
||||||
|
case "set" -> {
|
||||||
|
String key = (String) input.get("key");
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
yield errorJson("'set' action requires 'key' parameter");
|
||||||
|
}
|
||||||
|
yield executeSet(key, input, configStore);
|
||||||
|
}
|
||||||
|
case "list" -> executeList(configStore);
|
||||||
|
default -> errorJson("Invalid action: '" + action + "'. Valid values: get, set, list");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +128,9 @@ public class ConfigTool implements Tool {
|
|||||||
public String activityDescription(Map<String, Object> input) {
|
public String activityDescription(Map<String, Object> input) {
|
||||||
String action = (String) input.getOrDefault("action", "?");
|
String action = (String) input.getOrDefault("action", "?");
|
||||||
String key = (String) input.getOrDefault("key", "?");
|
String key = (String) input.getOrDefault("key", "?");
|
||||||
|
if ("list".equalsIgnoreCase(action)) {
|
||||||
|
return "⚙️ Listing all config";
|
||||||
|
}
|
||||||
if ("set".equalsIgnoreCase(action)) {
|
if ("set".equalsIgnoreCase(action)) {
|
||||||
return "⚙️ Setting config: " + key;
|
return "⚙️ Setting config: " + key;
|
||||||
}
|
}
|
||||||
@@ -129,119 +138,187 @@ public class ConfigTool implements Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* get / set 具体实现 */
|
/* get / set / list 具体实现 */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行 get 操作:优先从配置映射读取,回退到系统属性。
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @param configStore 配置映射
|
|
||||||
* @return JSON 格式的结果
|
|
||||||
*/
|
|
||||||
private String executeGet(String key, ConcurrentHashMap<String, String> configStore) {
|
private String executeGet(String key, ConcurrentHashMap<String, String> configStore) {
|
||||||
// 优先从上下文配置映射获取
|
|
||||||
String value = configStore.get(key);
|
String value = configStore.get(key);
|
||||||
|
|
||||||
// 回退到系统属性
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
value = System.getProperty(key);
|
value = System.getProperty(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return """
|
return """
|
||||||
{
|
{"action": "get", "key": "%s", "value": null, "found": false, \
|
||||||
"action": "get",
|
"message": "Config key '%s' not found"}"""
|
||||||
"key": "%s",
|
.formatted(escapeJson(key), escapeJson(key));
|
||||||
"value": null,
|
|
||||||
"found": false,
|
|
||||||
"message": "Config key '%s' not found"
|
|
||||||
}""".formatted(escapeJson(key), escapeJson(key));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return """
|
return """
|
||||||
{
|
{"action": "get", "key": "%s", "value": "%s", "found": true}"""
|
||||||
"action": "get",
|
.formatted(escapeJson(key), escapeJson(value));
|
||||||
"key": "%s",
|
|
||||||
"value": "%s",
|
|
||||||
"found": true
|
|
||||||
}""".formatted(escapeJson(key), escapeJson(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行 set 操作:同时写入配置映射和系统属性。
|
|
||||||
*
|
|
||||||
* @param key 配置键
|
|
||||||
* @param input 输入参数映射
|
|
||||||
* @param configStore 配置映射
|
|
||||||
* @return JSON 格式的确认
|
|
||||||
*/
|
|
||||||
private String executeSet(String key, Map<String, Object> input,
|
private String executeSet(String key, Map<String, Object> input,
|
||||||
ConcurrentHashMap<String, String> configStore) {
|
ConcurrentHashMap<String, String> configStore) {
|
||||||
String value = (String) input.get("value");
|
String value = (String) input.get("value");
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return errorJson("set operation requires 'value' parameter");
|
return errorJson("'set' action requires 'value' parameter");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取旧值(用于返回信息)
|
|
||||||
String oldValue = configStore.get(key);
|
String oldValue = configStore.get(key);
|
||||||
if (oldValue == null) {
|
|
||||||
oldValue = System.getProperty(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入配置映射
|
|
||||||
configStore.put(key, value);
|
configStore.put(key, value);
|
||||||
|
|
||||||
// 同步写入系统属性(简单实现,生产环境应使用专门的配置管理)
|
// 持久化到文件
|
||||||
try {
|
persistToFile(configStore);
|
||||||
System.setProperty(key, value);
|
|
||||||
} catch (SecurityException e) {
|
StringBuilder sb = new StringBuilder();
|
||||||
// 如果没有权限设置系统属性,只使用配置映射即可
|
sb.append("{\"action\": \"set\", \"key\": \"").append(escapeJson(key));
|
||||||
|
sb.append("\", \"value\": \"").append(escapeJson(value)).append("\"");
|
||||||
|
if (oldValue != null) {
|
||||||
|
sb.append(", \"previous_value\": \"").append(escapeJson(oldValue)).append("\"");
|
||||||
|
}
|
||||||
|
sb.append(", \"success\": true}");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String executeList(ConcurrentHashMap<String, String> configStore) {
|
||||||
|
if (configStore.isEmpty()) {
|
||||||
|
return "{\"action\": \"list\", \"count\": 0, \"message\": \"No configuration set\"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("{\n");
|
sb.append("{\"action\": \"list\", \"count\": ").append(configStore.size());
|
||||||
sb.append(" \"action\": \"set\",\n");
|
sb.append(", \"settings\": {");
|
||||||
sb.append(" \"key\": \"").append(escapeJson(key)).append("\",\n");
|
boolean first = true;
|
||||||
sb.append(" \"value\": \"").append(escapeJson(value)).append("\",\n");
|
for (var entry : new java.util.TreeMap<>(configStore).entrySet()) {
|
||||||
|
if (!first) sb.append(", ");
|
||||||
if (oldValue != null) {
|
sb.append("\"").append(escapeJson(entry.getKey())).append("\": \"")
|
||||||
sb.append(" \"previous_value\": \"").append(escapeJson(oldValue)).append("\",\n");
|
.append(escapeJson(entry.getValue())).append("\"");
|
||||||
} else {
|
first = false;
|
||||||
sb.append(" \"previous_value\": null,\n");
|
|
||||||
}
|
}
|
||||||
|
sb.append("}}");
|
||||||
sb.append(" \"success\": true,\n");
|
|
||||||
sb.append(" \"message\": \"Config key '").append(escapeJson(key)).append("' has been set\"\n");
|
|
||||||
sb.append("}");
|
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* 辅助方法 */
|
/* 持久化 — JSON 文件读写 */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
@SuppressWarnings("unchecked")
|
||||||
* 转义 JSON 特殊字符。
|
private ConcurrentHashMap<String, String> getOrInitStore(ToolContext context) {
|
||||||
*/
|
ConcurrentHashMap<String, String> store = context.getOrDefault(CONFIG_STORE_KEY, null);
|
||||||
private String escapeJson(String s) {
|
if (store != null) {
|
||||||
if (s == null) return "";
|
return store;
|
||||||
return s.replace("\\", "\\\\")
|
}
|
||||||
.replace("\"", "\\\"")
|
|
||||||
.replace("\n", "\\n")
|
// 从文件加载
|
||||||
.replace("\r", "\\r")
|
store = loadFromFile();
|
||||||
.replace("\t", "\\t");
|
context.set(CONFIG_STORE_KEY, store);
|
||||||
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建错误 JSON 响应。
|
* 从 JSON 文件加载配置。
|
||||||
|
* 使用简单的手动 JSON 解析,避免引入额外依赖。
|
||||||
*/
|
*/
|
||||||
|
private ConcurrentHashMap<String, String> loadFromFile() {
|
||||||
|
ConcurrentHashMap<String, String> store = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
if (!Files.exists(CONFIG_FILE)) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = Files.readString(CONFIG_FILE, StandardCharsets.UTF_8).trim();
|
||||||
|
if (json.startsWith("{") && json.endsWith("}")) {
|
||||||
|
// Simple JSON parsing for flat key-value pairs
|
||||||
|
String inner = json.substring(1, json.length() - 1).trim();
|
||||||
|
if (!inner.isEmpty()) {
|
||||||
|
parseJsonPairs(inner, store);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug("Loaded {} config entries from {}", store.size(), CONFIG_FILE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to load config from {}: {}", CONFIG_FILE, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持久化配置到 JSON 文件。
|
||||||
|
*/
|
||||||
|
private void persistToFile(ConcurrentHashMap<String, String> store) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(CONFIG_FILE.getParent());
|
||||||
|
|
||||||
|
StringBuilder json = new StringBuilder("{\n");
|
||||||
|
boolean first = true;
|
||||||
|
for (var entry : new java.util.TreeMap<>(store).entrySet()) {
|
||||||
|
if (!first) json.append(",\n");
|
||||||
|
json.append(" \"").append(escapeJson(entry.getKey()))
|
||||||
|
.append("\": \"").append(escapeJson(entry.getValue())).append("\"");
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
json.append("\n}");
|
||||||
|
|
||||||
|
Files.writeString(CONFIG_FILE, json.toString(), StandardCharsets.UTF_8);
|
||||||
|
log.debug("Persisted {} config entries to {}", store.size(), CONFIG_FILE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to persist config to {}: {}", CONFIG_FILE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单解析 JSON 键值对(仅支持字符串值的扁平对象)。
|
||||||
|
*/
|
||||||
|
private void parseJsonPairs(String inner, Map<String, String> store) {
|
||||||
|
int i = 0;
|
||||||
|
while (i < inner.length()) {
|
||||||
|
// Find key
|
||||||
|
int keyStart = inner.indexOf('"', i);
|
||||||
|
if (keyStart < 0) break;
|
||||||
|
int keyEnd = findClosingQuote(inner, keyStart + 1);
|
||||||
|
if (keyEnd < 0) break;
|
||||||
|
String key = unescapeJson(inner.substring(keyStart + 1, keyEnd));
|
||||||
|
|
||||||
|
// Find colon
|
||||||
|
int colon = inner.indexOf(':', keyEnd + 1);
|
||||||
|
if (colon < 0) break;
|
||||||
|
|
||||||
|
// Find value
|
||||||
|
int valStart = inner.indexOf('"', colon + 1);
|
||||||
|
if (valStart < 0) break;
|
||||||
|
int valEnd = findClosingQuote(inner, valStart + 1);
|
||||||
|
if (valEnd < 0) break;
|
||||||
|
String value = unescapeJson(inner.substring(valStart + 1, valEnd));
|
||||||
|
|
||||||
|
store.put(key, value);
|
||||||
|
i = valEnd + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findClosingQuote(String s, int from) {
|
||||||
|
for (int i = from; i < s.length(); i++) {
|
||||||
|
if (s.charAt(i) == '\\') { i++; continue; }
|
||||||
|
if (s.charAt(i) == '"') return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String unescapeJson(String s) {
|
||||||
|
return s.replace("\\\"", "\"").replace("\\\\", "\\")
|
||||||
|
.replace("\\n", "\n").replace("\\t", "\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
|
||||||
|
}
|
||||||
|
|
||||||
private String errorJson(String message) {
|
private String errorJson(String message) {
|
||||||
return """
|
return "{\"error\": true, \"message\": \"%s\"}".formatted(escapeJson(message));
|
||||||
{
|
|
||||||
"error": true,
|
|
||||||
"message": "%s"
|
|
||||||
}""".formatted(escapeJson(message));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.claudecode.tool.impl;
|
||||||
|
|
||||||
|
import com.claudecode.tool.Tool;
|
||||||
|
import com.claudecode.tool.ToolContext;
|
||||||
|
import com.claudecode.tool.ToolRegistry;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具搜索工具 —— 对应 claude-code 中 /tools 命令功能。
|
||||||
|
* <p>
|
||||||
|
* 在 ToolRegistry 中搜索已注册的工具,按名称或描述关键字匹配。
|
||||||
|
* 用于帮助 LLM 发现可用工具。
|
||||||
|
*/
|
||||||
|
public class ToolSearchTool implements Tool {
|
||||||
|
|
||||||
|
private static final String TOOL_REGISTRY_KEY = "TOOL_REGISTRY";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "ToolSearch";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return """
|
||||||
|
Search for available tools by name or keyword. Returns matching tool names and \
|
||||||
|
their descriptions. Use this when you need to find which tools are available for \
|
||||||
|
a specific task, or when the user asks about available capabilities.""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query to match against tool names and descriptions. Empty string lists all tools."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
|
String query = (String) input.get("query");
|
||||||
|
if (query == null) query = "";
|
||||||
|
String queryLower = query.toLowerCase().trim();
|
||||||
|
|
||||||
|
ToolRegistry registry = context.getOrDefault(TOOL_REGISTRY_KEY, null);
|
||||||
|
if (registry == null) {
|
||||||
|
return "Error: ToolRegistry is not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Tool> allTools = registry.getTools();
|
||||||
|
List<Tool> matches;
|
||||||
|
|
||||||
|
if (queryLower.isEmpty()) {
|
||||||
|
matches = allTools;
|
||||||
|
} else {
|
||||||
|
matches = allTools.stream()
|
||||||
|
.filter(t -> t.name().toLowerCase().contains(queryLower)
|
||||||
|
|| t.description().toLowerCase().contains(queryLower))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
return String.format("""
|
||||||
|
{"query": "%s", "total_tools": %d, "matches": 0, \
|
||||||
|
"message": "No tools matched the query."}""",
|
||||||
|
escapeJson(query), allTools.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(String.format("Found %d tool(s)", matches.size()));
|
||||||
|
if (!queryLower.isEmpty()) {
|
||||||
|
sb.append(" matching \"").append(query).append("\"");
|
||||||
|
}
|
||||||
|
sb.append(" (").append(allTools.size()).append(" total):\n\n");
|
||||||
|
|
||||||
|
for (Tool t : matches) {
|
||||||
|
sb.append("• **").append(t.name()).append("**");
|
||||||
|
if (t.isReadOnly()) sb.append(" [read-only]");
|
||||||
|
sb.append("\n");
|
||||||
|
// Truncate description to first 120 chars for overview
|
||||||
|
String desc = t.description().strip();
|
||||||
|
if (desc.length() > 120) {
|
||||||
|
desc = desc.substring(0, 117) + "...";
|
||||||
|
}
|
||||||
|
sb.append(" ").append(desc).append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString().stripTrailing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String activityDescription(Map<String, Object> input) {
|
||||||
|
return "🔍 Searching tools: " + input.getOrDefault("query", "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJson(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user