66import io .github .hiwepy .opencode .OpenCodeClientConfig ;
77import io .github .hiwepy .opencode .exception .OpenCodeHttpException ;
88import io .github .hiwepy .opencode .model .*;
9- import kong .unirest .core .*;
10- import kong .unirest .jackson .JacksonObjectMapper ;
9+ import org .apache .http .client .config .RequestConfig ;
10+ import org .apache .http .client .methods .*;
11+ import org .apache .http .conn .ssl .NoopHostnameVerifier ;
12+ import org .apache .http .conn .ssl .SSLConnectionSocketFactory ;
13+ import org .apache .http .entity .ContentType ;
14+ import org .apache .http .entity .StringEntity ;
15+ import org .apache .http .impl .client .CloseableHttpClient ;
16+ import org .apache .http .impl .client .HttpClients ;
17+ import org .apache .http .ssl .SSLContextBuilder ;
18+ import org .apache .http .util .EntityUtils ;
1119import org .slf4j .Logger ;
1220import org .slf4j .LoggerFactory ;
1321
22+ import javax .net .ssl .SSLContext ;
23+ import java .nio .charset .StandardCharsets ;
1424import java .util .Base64 ;
1525import java .util .List ;
1626import java .util .Map ;
27+ import java .util .Objects ;
1728
1829/**
1930 * OpenCode Server HTTP 客户端,封装 REST API。
2334public class OpenCodeHttpClient implements AutoCloseable {
2435
2536 private static final Logger log = LoggerFactory .getLogger (OpenCodeHttpClient .class );
37+ private static final ObjectMapper MAPPER = new ObjectMapper ()
38+ .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false );
2639
2740 private final OpenCodeClientConfig config ;
28- private final UnirestInstance unirest ;
41+ private final CloseableHttpClient http ;
2942
3043 public OpenCodeHttpClient (OpenCodeClientConfig config ) {
31- this .config = config ;
32- ObjectMapper mapper = new ObjectMapper ()
33- .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false );
34- this .unirest = Unirest .primaryInstance ();
35- this .unirest .config ()
36- .baseUrl (config .getServerUrl ())
37- .connectTimeout (config .getConnectTimeoutMillis ())
38- .socketTimeout (config .getReadTimeoutMillis ())
39- .verifySsl (config .isVerifySsl ())
40- .setObjectMapper (new JacksonObjectMapper (mapper ));
41-
42- String password = config .resolvePassword ();
43- if (!password .isEmpty ()) {
44- String credentials = Base64 .getEncoder ()
45- .encodeToString ((config .getUsername () + ":" + password ).getBytes ());
46- this .unirest .config ().addDefaultHeader ("Authorization" , "Basic " + credentials );
47- }
44+ this .config = Objects .requireNonNull (config , "config" );
45+ this .http = buildHttpClient (config );
4846 }
4947
5048 // ============================================================
@@ -69,12 +67,12 @@ public Session getSession(String sessionId) {
6967 }
7068
7169 public List <Session > listSessions () {
72- return get ("/session" , new TypeReference <List <Session >>() {});
70+ String json = getRaw ("/session" );
71+ return parseList (json , new TypeReference <List <Session >>() {});
7372 }
7473
7574 public boolean deleteSession (String sessionId ) {
76- HttpResponse <String > resp = unirest .delete ("/session/" + sessionId ).asString ();
77- return resp .isSuccess ();
75+ return delete ("/session/" + sessionId );
7876 }
7977
8078 // ============================================================
@@ -83,86 +81,185 @@ public boolean deleteSession(String sessionId) {
8381
8482 /**
8583 * 发送消息并同步等待 AI 响应(POST /session/:id/message)。
86- * <p>此方法会阻塞直到 AI 完成响应,可能耗时较长。</p>
8784 */
8885 public PromptResult prompt (String sessionId , PromptRequest request ) {
8986 return post ("/session/" + sessionId + "/message" , request , PromptResult .class );
9087 }
9188
9289 /**
9390 * 异步发送消息,不等待响应(POST /session/:id/prompt_async)。
94- * <p>返回 204 No Content 表示已接受。</p>
95- *
96- * @return 是否成功提交
9791 */
9892 public boolean promptAsync (String sessionId , PromptRequest request ) {
99- HttpResponse <String > resp = unirest .post ("/session/" + sessionId + "/prompt_async" )
100- .body (request )
101- .asString ();
102- return resp .isSuccess ();
93+ return postNoContent ("/session/" + sessionId + "/prompt_async" , request );
10394 }
10495
10596 public List <PromptResult > getMessages (String sessionId ) {
106- return get ("/session/" + sessionId + "/message" , new TypeReference <List <PromptResult >>() {});
97+ String json = getRaw ("/session/" + sessionId + "/message" );
98+ return parseList (json , new TypeReference <List <PromptResult >>() {});
10799 }
108100
109- /**
110- * 中止正在运行的会话。
111- */
112101 public boolean abortSession (String sessionId ) {
113- HttpResponse <String > resp = unirest .post ("/session/" + sessionId + "/abort" ).asString ();
114- return resp .isSuccess ();
102+ return postNoContent ("/session/" + sessionId + "/abort" , null );
115103 }
116104
117105 // ============================================================
118106 // Agent
119107 // ============================================================
120108
121109 public List <Agent > listAgents () {
122- return get ("/agent" , new TypeReference <List <Agent >>() {});
110+ String json = getRaw ("/agent" );
111+ return parseList (json , new TypeReference <List <Agent >>() {});
123112 }
124113
125114 // ============================================================
126- // MCP
115+ // Internal HTTP helpers
127116 // ============================================================
128117
129- public Map <String , Object > getMcpStatus () {
130- return get ("/mcp" , new TypeReference <Map <String , Object >>() {});
118+ private <T > T get (String path , Class <T > type ) {
119+ String json = getRaw (path );
120+ try {
121+ return MAPPER .readValue (json , type );
122+ } catch (Exception e ) {
123+ throw new OpenCodeHttpException (200 , "JSON parse error: " + e .getMessage ());
124+ }
131125 }
132126
133- // ============================================================
134- // Internal helpers
135- // ============================================================
127+ private String getRaw (String path ) {
128+ String url = config .getServerUrl () + path ;
129+ HttpGet request = new HttpGet (url );
130+ addAuthHeader (request );
131+ try (CloseableHttpResponse response = http .execute (request )) {
132+ int status = response .getStatusLine ().getStatusCode ();
133+ String body = response .getEntity () != null
134+ ? EntityUtils .toString (response .getEntity (), StandardCharsets .UTF_8 ) : "" ;
135+ if (status < 200 || status >= 300 ) {
136+ throw new OpenCodeHttpException (status , body );
137+ }
138+ return body ;
139+ } catch (OpenCodeHttpException e ) {
140+ throw e ;
141+ } catch (Exception e ) {
142+ throw new OpenCodeHttpException (0 , "Request failed: " + e .getMessage ());
143+ }
144+ }
136145
137- private <T > T get (String path , Class <T > type ) {
138- HttpResponse <T > resp = unirest .get (path ).asObject (type );
139- if (!resp .isSuccess ()) {
140- throw new OpenCodeHttpException (resp .getStatus (), resp .getBody () != null ? resp .getBody ().toString () : "" );
146+ private <T > T post (String path , Object body , Class <T > type ) {
147+ String json = postRaw (path , body );
148+ try {
149+ return MAPPER .readValue (json , type );
150+ } catch (Exception e ) {
151+ throw new OpenCodeHttpException (200 , "JSON parse error: " + e .getMessage ());
141152 }
142- return resp .getBody ();
143153 }
144154
145- private <T > T get (String path , TypeReference <T > typeRef ) {
146- HttpResponse <T > resp = unirest .get (path ).asObject (typeRef );
147- if (!resp .isSuccess ()) {
148- throw new OpenCodeHttpException (resp .getStatus (), resp .getBody () != null ? resp .getBody ().toString () : "" );
155+ private boolean postNoContent (String path , Object body ) {
156+ String url = config .getServerUrl () + path ;
157+ HttpPost request = new HttpPost (url );
158+ addAuthHeader (request );
159+ if (body != null ) {
160+ try {
161+ String json = MAPPER .writeValueAsString (body );
162+ request .setEntity (new StringEntity (json , ContentType .APPLICATION_JSON ));
163+ } catch (Exception e ) {
164+ throw new OpenCodeHttpException (0 , "JSON serialize error: " + e .getMessage ());
165+ }
166+ }
167+ try (CloseableHttpResponse response = http .execute (request )) {
168+ int status = response .getStatusLine ().getStatusCode ();
169+ return status >= 200 && status < 300 ;
170+ } catch (Exception e ) {
171+ return false ;
149172 }
150- return resp .getBody ();
151173 }
152174
153- private <T > T post (String path , Object body , Class <T > type ) {
154- HttpResponse <T > resp = unirest .post (path )
155- .header ("Content-Type" , "application/json" )
156- .body (body )
157- .asObject (type );
158- if (!resp .isSuccess ()) {
159- throw new OpenCodeHttpException (resp .getStatus (), resp .getBody () != null ? resp .getBody ().toString () : "" );
175+ private String postRaw (String path , Object body ) {
176+ String url = config .getServerUrl () + path ;
177+ HttpPost request = new HttpPost (url );
178+ addAuthHeader (request );
179+ if (body != null ) {
180+ try {
181+ String json = MAPPER .writeValueAsString (body );
182+ request .setEntity (new StringEntity (json , ContentType .APPLICATION_JSON ));
183+ } catch (Exception e ) {
184+ throw new OpenCodeHttpException (0 , "JSON serialize error: " + e .getMessage ());
185+ }
186+ }
187+ try (CloseableHttpResponse response = http .execute (request )) {
188+ int status = response .getStatusLine ().getStatusCode ();
189+ String responseBody = response .getEntity () != null
190+ ? EntityUtils .toString (response .getEntity (), StandardCharsets .UTF_8 ) : "" ;
191+ if (status < 200 || status >= 300 ) {
192+ throw new OpenCodeHttpException (status , responseBody );
193+ }
194+ return responseBody ;
195+ } catch (OpenCodeHttpException e ) {
196+ throw e ;
197+ } catch (Exception e ) {
198+ throw new OpenCodeHttpException (0 , "Request failed: " + e .getMessage ());
199+ }
200+ }
201+
202+ private boolean delete (String path ) {
203+ String url = config .getServerUrl () + path ;
204+ HttpDelete request = new HttpDelete (url );
205+ addAuthHeader (request );
206+ try (CloseableHttpResponse response = http .execute (request )) {
207+ int status = response .getStatusLine ().getStatusCode ();
208+ return status >= 200 && status < 300 ;
209+ } catch (Exception e ) {
210+ return false ;
211+ }
212+ }
213+
214+ private void addAuthHeader (HttpRequestBase request ) {
215+ String password = config .resolvePassword ();
216+ if (!password .isEmpty ()) {
217+ String credentials = Base64 .getEncoder ()
218+ .encodeToString ((config .getUsername () + ":" + password ).getBytes ());
219+ request .setHeader ("Authorization" , "Basic " + credentials );
220+ }
221+ }
222+
223+ private <T > List <T > parseList (String json , TypeReference <List <T >> typeRef ) {
224+ try {
225+ return MAPPER .readValue (json , typeRef );
226+ } catch (Exception e ) {
227+ throw new OpenCodeHttpException (200 , "JSON parse error: " + e .getMessage ());
228+ }
229+ }
230+
231+ private CloseableHttpClient buildHttpClient (OpenCodeClientConfig config ) {
232+ try {
233+ RequestConfig requestConfig = RequestConfig .custom ()
234+ .setConnectTimeout (config .getConnectTimeoutMillis ())
235+ .setSocketTimeout (config .getReadTimeoutMillis ())
236+ .setConnectionRequestTimeout (config .getConnectTimeoutMillis ())
237+ .build ();
238+
239+ if (!config .isVerifySsl ()) {
240+ SSLContext sslContext = new SSLContextBuilder ()
241+ .loadTrustMaterial (null , (chain , authType ) -> true )
242+ .build ();
243+ SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory (
244+ sslContext , NoopHostnameVerifier .INSTANCE );
245+ return HttpClients .custom ()
246+ .setDefaultRequestConfig (requestConfig )
247+ .setSSLSocketFactory (sslFactory )
248+ .build ();
249+ }
250+ return HttpClients .custom ()
251+ .setDefaultRequestConfig (requestConfig )
252+ .build ();
253+ } catch (Exception e ) {
254+ throw new RuntimeException ("Failed to build HTTP client" , e );
160255 }
161- return resp .getBody ();
162256 }
163257
164258 @ Override
165259 public void close () {
166- unirest .shutDown ();
260+ try {
261+ http .close ();
262+ } catch (Exception ignored ) {
263+ }
167264 }
168265}
0 commit comments