|
|
@@ -1,7 +1,5 @@
|
|
1
|
1
|
package com.huimv.employment.integration.kb;
|
|
2
|
2
|
|
|
3
|
|
-import com.fasterxml.jackson.databind.JsonNode;
|
|
4
|
|
-import com.fasterxml.jackson.databind.ObjectMapper;
|
|
5
|
3
|
import com.huimv.employment.common.exception.BizException;
|
|
6
|
4
|
import com.huimv.employment.common.exception.ErrorCode;
|
|
7
|
5
|
import org.springframework.beans.factory.annotation.Qualifier;
|
|
|
@@ -24,21 +22,22 @@ import java.net.URL;
|
|
24
|
22
|
import java.nio.charset.StandardCharsets;
|
|
25
|
23
|
|
|
26
|
24
|
/**
|
|
27
|
|
- * 将 OpenAI 兼容对话/模型列表接口转发至上游大模型网关。
|
|
|
25
|
+ * 将智能体 {@code /api/version}、{@code /api/console/chat} 转发至上游网关。
|
|
28
|
26
|
*/
|
|
29
|
27
|
@Service
|
|
30
|
28
|
public class KbOpenAiProxyServiceImpl implements KbOpenAiProxyService {
|
|
31
|
29
|
|
|
|
30
|
+ private static final String HEADER_AGENT_ID = "X-Agent-Id";
|
|
32
|
31
|
private static final String PATH_VERSION = "/api/version";
|
|
33
|
32
|
private static final String PATH_CHAT = "/api/console/chat";
|
|
34
|
33
|
|
|
35
|
34
|
private final KbApiProperties properties;
|
|
36
|
35
|
private final RestTemplate restTemplate;
|
|
37
|
|
- private final ObjectMapper objectMapper;
|
|
|
36
|
+ private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
|
38
|
37
|
|
|
39
|
38
|
public KbOpenAiProxyServiceImpl(KbApiProperties properties,
|
|
40
|
39
|
@Qualifier("kbRestTemplate") RestTemplate restTemplate,
|
|
41
|
|
- ObjectMapper objectMapper) {
|
|
|
40
|
+ com.fasterxml.jackson.databind.ObjectMapper objectMapper) {
|
|
42
|
41
|
this.properties = properties;
|
|
43
|
42
|
this.restTemplate = restTemplate;
|
|
44
|
43
|
this.objectMapper = objectMapper;
|
|
|
@@ -46,12 +45,12 @@ public class KbOpenAiProxyServiceImpl implements KbOpenAiProxyService {
|
|
46
|
45
|
|
|
47
|
46
|
@Override
|
|
48
|
47
|
public ResponseEntity<byte[]> getVersion() {
|
|
49
|
|
- ensureReady();
|
|
|
48
|
+ ensureEnabled();
|
|
50
|
49
|
try {
|
|
51
|
50
|
ResponseEntity<byte[]> upstream = restTemplate.exchange(
|
|
52
|
51
|
properties.baseUrl() + PATH_VERSION,
|
|
53
|
52
|
HttpMethod.GET,
|
|
54
|
|
- new HttpEntity<>(upstreamAuthHeaders()),
|
|
|
53
|
+ new HttpEntity<>(upstreamHeaders(null, false)),
|
|
55
|
54
|
byte[].class);
|
|
56
|
55
|
return forward(upstream);
|
|
57
|
56
|
} catch (HttpStatusCodeException ex) {
|
|
|
@@ -60,38 +59,16 @@ public class KbOpenAiProxyServiceImpl implements KbOpenAiProxyService {
|
|
60
|
59
|
}
|
|
61
|
60
|
|
|
62
|
61
|
@Override
|
|
63
|
|
- public ResponseEntity<byte[]> consoleChat(byte[] requestBody) {
|
|
64
|
|
- ensureReady();
|
|
65
|
|
- if (requestBody == null || requestBody.length == 0) {
|
|
66
|
|
- throw new BizException(ErrorCode.BAD_REQUEST, "请求体不能为空");
|
|
67
|
|
- }
|
|
68
|
|
- try {
|
|
69
|
|
- HttpHeaders headers = upstreamAuthHeaders();
|
|
70
|
|
- headers.setContentType(MediaType.APPLICATION_JSON);
|
|
71
|
|
- ResponseEntity<byte[]> upstream = restTemplate.exchange(
|
|
72
|
|
- properties.baseUrl() + PATH_CHAT,
|
|
73
|
|
- HttpMethod.POST,
|
|
74
|
|
- new HttpEntity<>(requestBody, headers),
|
|
75
|
|
- byte[].class);
|
|
76
|
|
- return forward(upstream);
|
|
77
|
|
- } catch (HttpStatusCodeException ex) {
|
|
78
|
|
- return forwardError(ex);
|
|
79
|
|
- }
|
|
80
|
|
- }
|
|
81
|
|
-
|
|
82
|
|
- @Override
|
|
83
|
|
- public void streamChatCompletions(byte[] requestBody, OutputStream output) throws IOException {
|
|
84
|
|
- ensureReady();
|
|
85
|
|
- if (requestBody == null || requestBody.length == 0) {
|
|
86
|
|
- throw new BizException(ErrorCode.BAD_REQUEST, "请求体不能为空");
|
|
87
|
|
- }
|
|
|
62
|
+ public void streamConsoleChat(byte[] requestBody, String agentId, OutputStream output) throws IOException {
|
|
|
63
|
+ ensureEnabled();
|
|
|
64
|
+ validateRequestBody(requestBody);
|
|
88
|
65
|
HttpURLConnection conn = null;
|
|
89
|
66
|
InputStream input = null;
|
|
90
|
67
|
try {
|
|
91
|
68
|
conn = openUpstreamConnection(requestBody.length);
|
|
92
|
69
|
conn.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
|
93
|
70
|
conn.setRequestProperty(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE);
|
|
94
|
|
- conn.setRequestProperty(properties.getAuthHeaderName(), properties.authHeaderValue());
|
|
|
71
|
+ applyUpstreamAuth(conn, agentId);
|
|
95
|
72
|
conn.setDoOutput(true);
|
|
96
|
73
|
try (OutputStream upstreamOut = conn.getOutputStream()) {
|
|
97
|
74
|
upstreamOut.write(requestBody);
|
|
|
@@ -100,16 +77,7 @@ public class KbOpenAiProxyServiceImpl implements KbOpenAiProxyService {
|
|
100
|
77
|
input = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
|
101
|
78
|
if (input != null) {
|
|
102
|
79
|
if (status >= 400) {
|
|
103
|
|
- byte[] errBytes = StreamUtils.copyToByteArray(input);
|
|
104
|
|
- if (errBytes.length > 0) {
|
|
105
|
|
- String errText = new String(errBytes, StandardCharsets.UTF_8).trim();
|
|
106
|
|
- if (errText.startsWith("data:") || errText.contains("\n\n")) {
|
|
107
|
|
- output.write(errBytes);
|
|
108
|
|
- output.flush();
|
|
109
|
|
- } else {
|
|
110
|
|
- SseStreamSupport.writeDataEvent(output, errText);
|
|
111
|
|
- }
|
|
112
|
|
- }
|
|
|
80
|
+ writeUpstreamError(output, input);
|
|
113
|
81
|
} else {
|
|
114
|
82
|
SseStreamSupport.pipeWithFlush(input, output);
|
|
115
|
83
|
}
|
|
|
@@ -129,16 +97,41 @@ public class KbOpenAiProxyServiceImpl implements KbOpenAiProxyService {
|
|
129
|
97
|
}
|
|
130
|
98
|
|
|
131
|
99
|
@Override
|
|
132
|
|
- public boolean isStreamRequest(byte[] requestBody) {
|
|
133
|
|
- if (requestBody == null || requestBody.length == 0) {
|
|
134
|
|
- return false;
|
|
135
|
|
- }
|
|
|
100
|
+ public String collectConsoleChatReply(byte[] requestBody, String agentId) {
|
|
|
101
|
+ ensureEnabled();
|
|
|
102
|
+ validateRequestBody(requestBody);
|
|
|
103
|
+ HttpURLConnection conn = null;
|
|
|
104
|
+ InputStream input = null;
|
|
136
|
105
|
try {
|
|
137
|
|
- JsonNode root = objectMapper.readTree(requestBody);
|
|
138
|
|
- JsonNode stream = root.get("stream");
|
|
139
|
|
- return stream != null && stream.asBoolean(false);
|
|
|
106
|
+ conn = openUpstreamConnection(requestBody.length);
|
|
|
107
|
+ conn.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
|
|
108
|
+ conn.setRequestProperty(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE);
|
|
|
109
|
+ applyUpstreamAuth(conn, agentId);
|
|
|
110
|
+ conn.setDoOutput(true);
|
|
|
111
|
+ try (OutputStream upstreamOut = conn.getOutputStream()) {
|
|
|
112
|
+ upstreamOut.write(requestBody);
|
|
|
113
|
+ }
|
|
|
114
|
+ int status = conn.getResponseCode();
|
|
|
115
|
+ input = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
|
|
116
|
+ if (status >= 400) {
|
|
|
117
|
+ throw upstreamHttpError(status, input);
|
|
|
118
|
+ }
|
|
|
119
|
+ return ConsoleChatSseParser.extractAssistantText(input, objectMapper);
|
|
|
120
|
+ } catch (BizException ex) {
|
|
|
121
|
+ throw ex;
|
|
140
|
122
|
} catch (Exception ex) {
|
|
141
|
|
- return false;
|
|
|
123
|
+ throw new BizException(ErrorCode.AI_SERVICE_FAILED, "调用大模型失败");
|
|
|
124
|
+ } finally {
|
|
|
125
|
+ if (input != null) {
|
|
|
126
|
+ try {
|
|
|
127
|
+ input.close();
|
|
|
128
|
+ } catch (IOException ignored) {
|
|
|
129
|
+ // ignore
|
|
|
130
|
+ }
|
|
|
131
|
+ }
|
|
|
132
|
+ if (conn != null) {
|
|
|
133
|
+ conn.disconnect();
|
|
|
134
|
+ }
|
|
142
|
135
|
}
|
|
143
|
136
|
}
|
|
144
|
137
|
|
|
|
@@ -152,18 +145,72 @@ public class KbOpenAiProxyServiceImpl implements KbOpenAiProxyService {
|
|
152
|
145
|
return conn;
|
|
153
|
146
|
}
|
|
154
|
147
|
|
|
155
|
|
- private void ensureReady() {
|
|
|
148
|
+ private void applyUpstreamAuth(HttpURLConnection conn, String agentId) {
|
|
|
149
|
+ conn.setRequestProperty(HEADER_AGENT_ID, resolveAgentId(agentId));
|
|
|
150
|
+ if (properties.hasUpstreamAuth()) {
|
|
|
151
|
+ conn.setRequestProperty(properties.getAuthHeaderName(), properties.authHeaderValue());
|
|
|
152
|
+ }
|
|
|
153
|
+ }
|
|
|
154
|
+
|
|
|
155
|
+ private void writeUpstreamError(OutputStream output, InputStream input) throws IOException {
|
|
|
156
|
+ byte[] errBytes = StreamUtils.copyToByteArray(input);
|
|
|
157
|
+ if (errBytes.length == 0) {
|
|
|
158
|
+ return;
|
|
|
159
|
+ }
|
|
|
160
|
+ String errText = new String(errBytes, StandardCharsets.UTF_8).trim();
|
|
|
161
|
+ if (errText.startsWith("data:") || errText.contains("\n\n")) {
|
|
|
162
|
+ output.write(errBytes);
|
|
|
163
|
+ output.flush();
|
|
|
164
|
+ } else {
|
|
|
165
|
+ SseStreamSupport.writeDataEvent(output, errText);
|
|
|
166
|
+ }
|
|
|
167
|
+ }
|
|
|
168
|
+
|
|
|
169
|
+ private BizException upstreamHttpError(int status, InputStream input) throws IOException {
|
|
|
170
|
+ if (input != null) {
|
|
|
171
|
+ byte[] errBytes = StreamUtils.copyToByteArray(input);
|
|
|
172
|
+ if (errBytes.length > 0) {
|
|
|
173
|
+ String errText = new String(errBytes, StandardCharsets.UTF_8).trim();
|
|
|
174
|
+ if (status == 401) {
|
|
|
175
|
+ String hint = properties.hasUpstreamAuth()
|
|
|
176
|
+ ? "大模型鉴权失败,请检查 fe.kb.api-key"
|
|
|
177
|
+ : "大模型鉴权失败";
|
|
|
178
|
+ return new BizException(ErrorCode.AI_SERVICE_FAILED, hint);
|
|
|
179
|
+ }
|
|
|
180
|
+ return new BizException(ErrorCode.AI_SERVICE_FAILED,
|
|
|
181
|
+ errText.length() > 200 ? errText.substring(0, 200) : errText);
|
|
|
182
|
+ }
|
|
|
183
|
+ }
|
|
|
184
|
+ return new BizException(ErrorCode.AI_SERVICE_FAILED, "上游返回 HTTP " + status);
|
|
|
185
|
+ }
|
|
|
186
|
+
|
|
|
187
|
+ private String resolveAgentId(String agentId) {
|
|
|
188
|
+ if (StringUtils.hasText(agentId)) {
|
|
|
189
|
+ return agentId.trim();
|
|
|
190
|
+ }
|
|
|
191
|
+ return StringUtils.hasText(properties.getDefaultAgentId())
|
|
|
192
|
+ ? properties.getDefaultAgentId().trim()
|
|
|
193
|
+ : "default";
|
|
|
194
|
+ }
|
|
|
195
|
+
|
|
|
196
|
+ private void validateRequestBody(byte[] requestBody) {
|
|
|
197
|
+ if (requestBody == null || requestBody.length == 0) {
|
|
|
198
|
+ throw new BizException(ErrorCode.BAD_REQUEST, "请求体不能为空");
|
|
|
199
|
+ }
|
|
|
200
|
+ }
|
|
|
201
|
+
|
|
|
202
|
+ private void ensureEnabled() {
|
|
156
|
203
|
if (!properties.isEnabled()) {
|
|
157
|
204
|
throw new BizException(ErrorCode.AI_SERVICE_DISABLED);
|
|
158
|
205
|
}
|
|
159
|
|
- if (!StringUtils.hasText(properties.getApiKey())) {
|
|
160
|
|
- throw new BizException(ErrorCode.AI_SERVICE_FAILED, "未配置大模型访问密钥 fe.kb.api-key");
|
|
161
|
|
- }
|
|
162
|
206
|
}
|
|
163
|
207
|
|
|
164
|
|
- private HttpHeaders upstreamAuthHeaders() {
|
|
|
208
|
+ private HttpHeaders upstreamHeaders(String agentId, boolean requireAgentId) {
|
|
165
|
209
|
HttpHeaders headers = new HttpHeaders();
|
|
166
|
|
- headers.add(properties.getAuthHeaderName(), properties.authHeaderValue());
|
|
|
210
|
+ properties.applyAuthHeader(headers);
|
|
|
211
|
+ if (requireAgentId) {
|
|
|
212
|
+ headers.add(HEADER_AGENT_ID, resolveAgentId(agentId));
|
|
|
213
|
+ }
|
|
167
|
214
|
return headers;
|
|
168
|
215
|
}
|
|
169
|
216
|
|