package com.company.agentfoundation.integration.ngp; import com.company.agentfoundation.config.FoundationProperties; import com.fasterxml.jackson.databind.JsonNode; import io.netty.channel.ChannelOption; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; import java.time.Duration; @Component public class NgpTokenProvider { private final FoundationProperties.Ngp config; private final WebClient webClient; private final Duration readTimeout; private volatile String accessToken; private volatile String refreshToken; public NgpTokenProvider(FoundationProperties properties, WebClient.Builder builder) { this.config = properties.getNgp(); HttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.max(config.getConnectTimeoutMs(), 1)) .responseTimeout(Duration.ofMillis(Math.max(config.getReadTimeoutMs(), 1))); this.webClient = builder.clone() .baseUrl(config.getBaseUrl()) .clientConnector(new ReactorClientHttpConnector(httpClient)) .codecs(codecs -> codecs.defaultCodecs() .maxInMemorySize(Math.max(config.getMaxResponseBytes(), 256 * 1024))) .build(); this.readTimeout = Duration.ofMillis(Math.max(config.getReadTimeoutMs(), 1)); this.accessToken = config.getAccessToken(); this.refreshToken = config.getRefreshToken(); } public String getAccessToken() { ensureBaseConfiguration(); if (!blank(accessToken)) return accessToken; return refreshAccessToken(); } public synchronized String refreshAccessToken() { ensureBaseConfiguration(); if (blank(refreshToken)) throw new NgpApiException("NGP access token 已失效,且未配置 refresh token"); try { JsonNode payload = webClient.post() .uri(builder -> builder.path(config.getTokenPath()) .queryParam("app_key", config.getAppKey()) .queryParam("app_secret", config.getAppSecret()) .queryParam("grant_type", "refresh_token") .queryParam("refresh_token", refreshToken) .build()) .accept(MediaType.APPLICATION_JSON) .contentLength(0) .retrieve() .bodyToMono(JsonNode.class) .block(readTimeout); JsonNode tokenData = findTokenData(payload); if (tokenData == null || blank(tokenData.path("access_token").asText())) { throw new NgpApiException("NGP token 刷新失败:" + apiMessage(payload)); } accessToken = tokenData.path("access_token").asText(); String nextRefreshToken = tokenData.path("refresh_token").asText(""); if (!nextRefreshToken.isBlank()) refreshToken = nextRefreshToken; return accessToken; } catch (NgpApiException exception) { throw exception; } catch (RuntimeException exception) { throw new NgpApiException("NGP token 刷新失败:" + rootMessage(exception), exception); } } private JsonNode findTokenData(JsonNode payload) { JsonNode current = payload; for (int depth = 0; depth < 4 && current != null && !current.isMissingNode() && !current.isNull(); depth++) { if (!blank(current.path("access_token").asText())) return current; current = current.path("data"); } return null; } private void ensureBaseConfiguration() { if (!config.isEnabled()) throw new NgpApiException("NGP 接口未启用"); if (blank(config.getAppKey()) || blank(config.getAppSecret())) { throw new NgpApiException("NGP 接口配置不完整:请设置 NGP_APP_KEY 和 NGP_APP_SECRET"); } } private String apiMessage(JsonNode payload) { if (payload == null || payload.isNull()) return "空响应"; return payload.path("message").asText(payload.path("msg").asText("未返回 access_token")); } private boolean blank(String value) { return value == null || value.isBlank(); } private String rootMessage(Throwable throwable) { Throwable current = throwable; while (current.getCause() != null && current.getCause() != current) current = current.getCause(); return current.getMessage() == null ? current.getClass().getSimpleName() : current.getMessage(); } }