mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
fix(mcp): expand JVM system properties in stdio args/env/cwd
This commit is contained in:
parent
bbf978ed89
commit
609da7029f
@ -5,6 +5,8 @@ import org.springframework.ai.chat.messages.AssistantMessage;
|
|||||||
import org.springframework.ai.chat.messages.Message;
|
import org.springframework.ai.chat.messages.Message;
|
||||||
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
|
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
|
||||||
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
|
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
|
||||||
|
import org.springframework.ai.chat.metadata.DefaultUsage;
|
||||||
|
import org.springframework.ai.chat.metadata.Usage;
|
||||||
import org.springframework.ai.chat.model.ChatModel;
|
import org.springframework.ai.chat.model.ChatModel;
|
||||||
import org.springframework.ai.chat.model.ChatResponse;
|
import org.springframework.ai.chat.model.ChatResponse;
|
||||||
import org.springframework.ai.chat.model.Generation;
|
import org.springframework.ai.chat.model.Generation;
|
||||||
@ -110,7 +112,22 @@ public class ChatGPTChatModel implements ChatModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
case "done" -> {
|
case "done" -> {
|
||||||
// 流结束,如果没有任何 tool call 产生过 done event 但有未完成的,忽略
|
// Emit a final empty-content ChatResponse carrying the
|
||||||
|
// usage metadata so NodeStreamingChatHelper can record
|
||||||
|
// per-turn token counts. Without this the OAuth path's
|
||||||
|
// turns log 0/0/0 (the client only knows about Usage
|
||||||
|
// when it's attached to a ChatResponse.metadata).
|
||||||
|
if (event.inputTokens() != null || event.outputTokens() != null
|
||||||
|
|| event.totalTokens() != null) {
|
||||||
|
int in = event.inputTokens() != null ? event.inputTokens() : 0;
|
||||||
|
int out = event.outputTokens() != null ? event.outputTokens() : 0;
|
||||||
|
int total = event.totalTokens() != null ? event.totalTokens() : (in + out);
|
||||||
|
Usage usage = new DefaultUsage(in, out, total);
|
||||||
|
Generation usageGen = new Generation(new AssistantMessage(""),
|
||||||
|
ChatGenerationMetadata.builder().finishReason("stop").build());
|
||||||
|
return new ChatResponse(List.of(usageGen),
|
||||||
|
ChatResponseMetadata.builder().model(model).usage(usage).build());
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
default -> { return null; }
|
default -> { return null; }
|
||||||
|
|||||||
@ -37,12 +37,17 @@ public class ChatGPTResponsesClient {
|
|||||||
/**
|
/**
|
||||||
* 流式调用结果 — 包含文本增量和 tool call 事件
|
* 流式调用结果 — 包含文本增量和 tool call 事件
|
||||||
*/
|
*/
|
||||||
public record StreamEvent(String type, String content, String toolCallId, String toolName, String toolArgsDelta) {
|
public record StreamEvent(String type, String content, String toolCallId, String toolName, String toolArgsDelta,
|
||||||
public static StreamEvent text(String delta) { return new StreamEvent("text", delta, null, null, null); }
|
Integer inputTokens, Integer outputTokens, Integer totalTokens) {
|
||||||
public static StreamEvent toolCallStart(String callId, String name) { return new StreamEvent("tool_call_start", null, callId, name, null); }
|
public static StreamEvent text(String delta) { return new StreamEvent("text", delta, null, null, null, null, null, null); }
|
||||||
public static StreamEvent toolCallArgsDelta(String callId, String delta) { return new StreamEvent("tool_call_args_delta", null, callId, null, delta); }
|
public static StreamEvent toolCallStart(String callId, String name) { return new StreamEvent("tool_call_start", null, callId, name, null, null, null, null); }
|
||||||
public static StreamEvent toolCallDone(String callId, String args) { return new StreamEvent("tool_call_done", null, callId, null, args); }
|
public static StreamEvent toolCallArgsDelta(String callId, String delta) { return new StreamEvent("tool_call_args_delta", null, callId, null, delta, null, null, null); }
|
||||||
public static StreamEvent done() { return new StreamEvent("done", null, null, null, null); }
|
public static StreamEvent toolCallDone(String callId, String args) { return new StreamEvent("tool_call_done", null, callId, null, args, null, null, null); }
|
||||||
|
public static StreamEvent done() { return new StreamEvent("done", null, null, null, null, null, null, null); }
|
||||||
|
/** Done event carrying token usage extracted from {@code response.completed.response.usage}. */
|
||||||
|
public static StreamEvent done(Integer in, Integer out, Integer total) {
|
||||||
|
return new StreamEvent("done", null, null, null, null, in, out, total);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -281,8 +286,25 @@ public class ChatGPTResponsesClient {
|
|||||||
return StreamEvent.toolCallDone(callId, args);
|
return StreamEvent.toolCallDone(callId, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 完成/结束
|
// 完成/结束 — extract usage from response.completed so the agent
|
||||||
|
// pipeline can record per-turn token counts (otherwise OAuth turns
|
||||||
|
// get logged as 0/0/0). Field shape mirrors the upstream client:
|
||||||
|
// input_tokens → prompt, output_tokens → completion, total_tokens
|
||||||
|
// falls back to (input + output) when the API omits it.
|
||||||
if (type.startsWith("response.completed") || type.startsWith("response.done")) {
|
if (type.startsWith("response.completed") || type.startsWith("response.done")) {
|
||||||
|
JsonNode usage = node.path("response").path("usage");
|
||||||
|
if (!usage.isMissingNode() && !usage.isNull()) {
|
||||||
|
Integer in = usage.has("input_tokens") ? usage.get("input_tokens").asInt() : null;
|
||||||
|
Integer out = usage.has("output_tokens") ? usage.get("output_tokens").asInt() : null;
|
||||||
|
Integer total = usage.has("total_tokens")
|
||||||
|
? usage.get("total_tokens").asInt()
|
||||||
|
: (in != null && out != null ? in + out : null);
|
||||||
|
if (in != null || out != null || total != null) {
|
||||||
|
log.debug("[ChatGPT] Usage in response.completed: input={}, output={}, total={}",
|
||||||
|
in, out, total);
|
||||||
|
return StreamEvent.done(in, out, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
return StreamEvent.done();
|
return StreamEvent.done();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -269,6 +269,7 @@ public class McpClientManager {
|
|||||||
// Args
|
// Args
|
||||||
if (server.getArgsJson() != null && !server.getArgsJson().isBlank()) {
|
if (server.getArgsJson() != null && !server.getArgsJson().isBlank()) {
|
||||||
List<String> args = JSONUtil.toList(server.getArgsJson(), String.class);
|
List<String> args = JSONUtil.toList(server.getArgsJson(), String.class);
|
||||||
|
args = args.stream().map(McpClientManager::expandEnvVars).toList();
|
||||||
builder.args(args);
|
builder.args(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,17 +377,22 @@ public class McpClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 展开环境变量引用,如 ${ENV_VAR} 或 $ENV_VAR
|
* 展开系统属性和环境变量引用,如 ${user.home}、${ENV_VAR} 或 $ENV_VAR
|
||||||
* <p>
|
* <p>
|
||||||
* 先处理 ${VAR}(精确匹配),再用正则处理 $VAR(word boundary),
|
* 先处理 ${VAR}(精确匹配,JVM 系统属性优先于环境变量,
|
||||||
* 避免 $PATH 误替换 $PATH_HOME 的问题。
|
* 这样 ${user.home} 等跨平台占位符在 Windows 上也能解析),
|
||||||
|
* 再用正则处理 $VAR(word boundary),避免 $PATH 误替换 $PATH_HOME 的问题。
|
||||||
*/
|
*/
|
||||||
private static String expandEnvVars(String value) {
|
private static String expandEnvVars(String value) {
|
||||||
if (value == null || !value.contains("$")) {
|
if (value == null || !value.contains("$")) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
String result = value;
|
String result = value;
|
||||||
// Phase 1: 精确匹配 ${VAR} 模式(不会误替换)
|
// Phase 1a: ${VAR} 优先匹配 JVM system property(如 ${user.home}、${java.io.tmpdir})
|
||||||
|
for (String key : System.getProperties().stringPropertyNames()) {
|
||||||
|
result = result.replace("${" + key + "}", System.getProperty(key));
|
||||||
|
}
|
||||||
|
// Phase 1b: ${VAR} 回退匹配 OS 环境变量
|
||||||
for (Map.Entry<String, String> env : System.getenv().entrySet()) {
|
for (Map.Entry<String, String> env : System.getenv().entrySet()) {
|
||||||
result = result.replace("${" + env.getKey() + "}", env.getValue());
|
result = result.replace("${" + env.getKey() + "}", env.getValue());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user