|
|
|
|
@ -2,35 +2,46 @@ package com.claudecode.tool.impl; |
|
|
|
|
|
|
|
|
|
import com.claudecode.tool.Tool; |
|
|
|
|
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.concurrent.ConcurrentHashMap; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Config 工具 —— 获取或设置配置值。 |
|
|
|
|
* Config 工具 —— 获取或设置配置值,持久化到 JSON 文件。 |
|
|
|
|
* <p> |
|
|
|
|
* 属于 P2 优先级的辅助工具。支持两种存储后端: |
|
|
|
|
* 属于 P2 优先级的辅助工具。存储后端优先级: |
|
|
|
|
* </p> |
|
|
|
|
* <ol> |
|
|
|
|
* <li>首选从 ToolContext 中的配置映射(键 "CONFIG_STORE")读写</li> |
|
|
|
|
* <li>回退到 {@link System#getProperty} / {@link System#setProperty}</li> |
|
|
|
|
* <li>ToolContext 中的内存缓存(CONFIG_STORE)—— 快速读写</li> |
|
|
|
|
* <li>JSON 文件持久化(~/.claude-code/config.json)—— 跨会话保持</li> |
|
|
|
|
* <li>回退到 {@link System#getProperty} —— 兜底读取</li> |
|
|
|
|
* </ol> |
|
|
|
|
* |
|
|
|
|
* <h3>参数</h3> |
|
|
|
|
* <ul> |
|
|
|
|
* <li><b>action</b>(必填)—— "get" 或 "set"</li> |
|
|
|
|
* <li><b>key</b>(必填)—— 配置键名</li> |
|
|
|
|
* <li><b>value</b>(可选,set 时必填)—— 配置值</li> |
|
|
|
|
* <li><b>action</b>(必填)—— "get"、"set" 或 "list"</li> |
|
|
|
|
* <li><b>key</b>(get/set 时必填)—— 配置键名</li> |
|
|
|
|
* <li><b>value</b>(set 时必填)—— 配置值</li> |
|
|
|
|
* </ul> |
|
|
|
|
* |
|
|
|
|
* <h3>返回</h3> |
|
|
|
|
* <p>JSON 格式:get 返回当前值,set 返回确认信息。</p> |
|
|
|
|
*/ |
|
|
|
|
public class ConfigTool implements Tool { |
|
|
|
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(ConfigTool.class); |
|
|
|
|
|
|
|
|
|
/** ToolContext 中配置存储的键名 */ |
|
|
|
|
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 |
|
|
|
|
public String name() { |
|
|
|
|
return "Config"; |
|
|
|
|
@ -39,8 +50,8 @@ public class ConfigTool implements Tool { |
|
|
|
|
@Override |
|
|
|
|
public String description() { |
|
|
|
|
return """ |
|
|
|
|
Get or set configuration values. Use when the user requests configuration changes, \ |
|
|
|
|
asks about current settings, or when adjusting a setting would benefit them. |
|
|
|
|
Get, set, or list configuration values. Configuration persists across sessions \ |
|
|
|
|
in ~/.claude-code/config.json. |
|
|
|
|
|
|
|
|
|
Available settings include: |
|
|
|
|
- language: Preferred response language (e.g., "zh-CN", "en") |
|
|
|
|
@ -50,7 +61,7 @@ public class ConfigTool implements Tool { |
|
|
|
|
- timeout: Default command timeout in seconds |
|
|
|
|
- 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 |
|
|
|
|
@ -61,26 +72,22 @@ public class ConfigTool implements Tool { |
|
|
|
|
"properties": { |
|
|
|
|
"action": { |
|
|
|
|
"type": "string", |
|
|
|
|
"description": "Action type: get or set", |
|
|
|
|
"enum": ["get", "set"] |
|
|
|
|
"description": "Action type: get, set, or list", |
|
|
|
|
"enum": ["get", "set", "list"] |
|
|
|
|
}, |
|
|
|
|
"key": { |
|
|
|
|
"type": "string", |
|
|
|
|
"description": "Configuration key name" |
|
|
|
|
"description": "Configuration key name (required for get/set)" |
|
|
|
|
}, |
|
|
|
|
"value": { |
|
|
|
|
"type": "string", |
|
|
|
|
"description": "Configuration value (required for set operation)" |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
"required": ["action", "key"] |
|
|
|
|
"required": ["action"] |
|
|
|
|
}"""; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Config 工具不是纯只读的(set 操作会修改状态), |
|
|
|
|
* 但出于安全考虑仍标记为 false。 |
|
|
|
|
*/ |
|
|
|
|
@Override |
|
|
|
|
public boolean isReadOnly() { |
|
|
|
|
return false; |
|
|
|
|
@ -88,33 +95,32 @@ public class ConfigTool implements Tool { |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public String execute(Map<String, Object> input, ToolContext context) { |
|
|
|
|
// 解析必填参数: action
|
|
|
|
|
String action = (String) input.get("action"); |
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
|
// 解析必填参数: key
|
|
|
|
|
String key = (String) input.get("key"); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
// 获取或初始化配置存储(从文件加载)
|
|
|
|
|
ConcurrentHashMap<String, String> configStore = getOrInitStore(context); |
|
|
|
|
|
|
|
|
|
return switch (action) { |
|
|
|
|
case "get" -> executeGet(key, configStore); |
|
|
|
|
case "set" -> executeSet(key, input, configStore); |
|
|
|
|
default -> errorJson("Invalid action value: '" + action + "'. Valid values: get, set"); |
|
|
|
|
case "get" -> { |
|
|
|
|
String key = (String) input.get("key"); |
|
|
|
|
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) { |
|
|
|
|
String action = (String) input.getOrDefault("action", "?"); |
|
|
|
|
String key = (String) input.getOrDefault("key", "?"); |
|
|
|
|
if ("list".equalsIgnoreCase(action)) { |
|
|
|
|
return "⚙️ Listing all config"; |
|
|
|
|
} |
|
|
|
|
if ("set".equalsIgnoreCase(action)) { |
|
|
|
|
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) { |
|
|
|
|
// 优先从上下文配置映射获取
|
|
|
|
|
String value = configStore.get(key); |
|
|
|
|
|
|
|
|
|
// 回退到系统属性
|
|
|
|
|
if (value == null) { |
|
|
|
|
value = System.getProperty(key); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (value == null) { |
|
|
|
|
return """ |
|
|
|
|
{ |
|
|
|
|
"action": "get", |
|
|
|
|
"key": "%s", |
|
|
|
|
"value": null, |
|
|
|
|
"found": false, |
|
|
|
|
"message": "Config key '%s' not found" |
|
|
|
|
}""".formatted(escapeJson(key), escapeJson(key)); |
|
|
|
|
{"action": "get", "key": "%s", "value": null, "found": false, \ |
|
|
|
|
"message": "Config key '%s' not found"}""" |
|
|
|
|
.formatted(escapeJson(key), escapeJson(key)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return """ |
|
|
|
|
{ |
|
|
|
|
"action": "get", |
|
|
|
|
"key": "%s", |
|
|
|
|
"value": "%s", |
|
|
|
|
"found": true |
|
|
|
|
}""".formatted(escapeJson(key), escapeJson(value)); |
|
|
|
|
{"action": "get", "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, |
|
|
|
|
ConcurrentHashMap<String, String> configStore) { |
|
|
|
|
String value = (String) input.get("value"); |
|
|
|
|
if (value == null) { |
|
|
|
|
return errorJson("set operation requires 'value' parameter"); |
|
|
|
|
return errorJson("'set' action requires 'value' parameter"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 获取旧值(用于返回信息)
|
|
|
|
|
String oldValue = configStore.get(key); |
|
|
|
|
if (oldValue == null) { |
|
|
|
|
oldValue = System.getProperty(key); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 写入配置映射
|
|
|
|
|
configStore.put(key, value); |
|
|
|
|
|
|
|
|
|
// 同步写入系统属性(简单实现,生产环境应使用专门的配置管理)
|
|
|
|
|
try { |
|
|
|
|
System.setProperty(key, value); |
|
|
|
|
} catch (SecurityException e) { |
|
|
|
|
// 如果没有权限设置系统属性,只使用配置映射即可
|
|
|
|
|
} |
|
|
|
|
// 持久化到文件
|
|
|
|
|
persistToFile(configStore); |
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder(); |
|
|
|
|
sb.append("{\n"); |
|
|
|
|
sb.append(" \"action\": \"set\",\n"); |
|
|
|
|
sb.append(" \"key\": \"").append(escapeJson(key)).append("\",\n"); |
|
|
|
|
sb.append(" \"value\": \"").append(escapeJson(value)).append("\",\n"); |
|
|
|
|
|
|
|
|
|
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("\",\n"); |
|
|
|
|
} else { |
|
|
|
|
sb.append(" \"previous_value\": null,\n"); |
|
|
|
|
sb.append(", \"previous_value\": \"").append(escapeJson(oldValue)).append("\""); |
|
|
|
|
} |
|
|
|
|
sb.append(", \"success\": true}"); |
|
|
|
|
return sb.toString(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
sb.append(" \"success\": true,\n"); |
|
|
|
|
sb.append(" \"message\": \"Config key '").append(escapeJson(key)).append("' has been set\"\n"); |
|
|
|
|
sb.append("}"); |
|
|
|
|
private String executeList(ConcurrentHashMap<String, String> configStore) { |
|
|
|
|
if (configStore.isEmpty()) { |
|
|
|
|
return "{\"action\": \"list\", \"count\": 0, \"message\": \"No configuration set\"}"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder(); |
|
|
|
|
sb.append("{\"action\": \"list\", \"count\": ").append(configStore.size()); |
|
|
|
|
sb.append(", \"settings\": {"); |
|
|
|
|
boolean first = true; |
|
|
|
|
for (var entry : new java.util.TreeMap<>(configStore).entrySet()) { |
|
|
|
|
if (!first) sb.append(", "); |
|
|
|
|
sb.append("\"").append(escapeJson(entry.getKey())).append("\": \"") |
|
|
|
|
.append(escapeJson(entry.getValue())).append("\""); |
|
|
|
|
first = false; |
|
|
|
|
} |
|
|
|
|
sb.append("}}"); |
|
|
|
|
return sb.toString(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------ */ |
|
|
|
|
/* 辅助方法 */ |
|
|
|
|
/* 持久化 — JSON 文件读写 */ |
|
|
|
|
/* ------------------------------------------------------------------ */ |
|
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked") |
|
|
|
|
private ConcurrentHashMap<String, String> getOrInitStore(ToolContext context) { |
|
|
|
|
ConcurrentHashMap<String, String> store = context.getOrDefault(CONFIG_STORE_KEY, null); |
|
|
|
|
if (store != null) { |
|
|
|
|
return store; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 从文件加载
|
|
|
|
|
store = loadFromFile(); |
|
|
|
|
context.set(CONFIG_STORE_KEY, store); |
|
|
|
|
return store; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 转义 JSON 特殊字符。 |
|
|
|
|
* 从 JSON 文件加载配置。 |
|
|
|
|
* 使用简单的手动 JSON 解析,避免引入额外依赖。 |
|
|
|
|
*/ |
|
|
|
|
private String escapeJson(String s) { |
|
|
|
|
if (s == null) return ""; |
|
|
|
|
return s.replace("\\", "\\\\") |
|
|
|
|
.replace("\"", "\\\"") |
|
|
|
|
.replace("\n", "\\n") |
|
|
|
|
.replace("\r", "\\r") |
|
|
|
|
.replace("\t", "\\t"); |
|
|
|
|
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 响应。 |
|
|
|
|
* 简单解析 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) { |
|
|
|
|
return """ |
|
|
|
|
{ |
|
|
|
|
"error": true, |
|
|
|
|
"message": "%s" |
|
|
|
|
}""".formatted(escapeJson(message)); |
|
|
|
|
return "{\"error\": true, \"message\": \"%s\"}".formatted(escapeJson(message)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|