001/**
002 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.gnu.org/licenses/lgpl-3.0.txt
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package dev.tinyflow.core.node;
017
018import com.agentsflex.core.chain.Chain;
019import com.agentsflex.core.chain.DataType;
020import com.agentsflex.core.chain.Parameter;
021import com.agentsflex.core.chain.node.BaseNode;
022import com.agentsflex.core.llm.client.OkHttpClientUtil;
023import com.agentsflex.core.prompt.template.TextPromptTemplate;
024import com.agentsflex.core.util.StringUtil;
025import com.alibaba.fastjson.JSON;
026import com.alibaba.fastjson.JSONObject;
027import okhttp3.*;
028
029import java.io.IOException;
030import java.io.UnsupportedEncodingException;
031import java.net.URLEncoder;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035
036public class HttpNode extends BaseNode {
037
038    private String url;
039    private String method;
040
041    private List<Parameter> headers;
042    private List<Parameter> urlParameters;
043
044    private String bodyType;
045    private List<Parameter> fromData;
046    private List<Parameter> fromUrlencoded;
047    private String bodyJson;
048    private String rawBody;
049
050    public static String mapToQueryString(Map<String, Object> map) {
051        if (map == null || map.isEmpty()) {
052            return "";
053        }
054
055        StringBuilder stringBuilder = new StringBuilder();
056
057        for (String key : map.keySet()) {
058            if (StringUtil.noText(key)) {
059                continue;
060            }
061            if (stringBuilder.length() > 0) {
062                stringBuilder.append("&");
063            }
064            stringBuilder.append(key.trim());
065            stringBuilder.append("=");
066            Object value = map.get(key);
067            stringBuilder.append(value == null ? "" : urlEncode(value.toString().trim()));
068        }
069        return stringBuilder.toString();
070    }
071
072    public static String urlEncode(String string) {
073        try {
074            return URLEncoder.encode(string, "UTF-8");
075        } catch (UnsupportedEncodingException e) {
076            throw new RuntimeException(e);
077        }
078    }
079
080    public String getUrl() {
081        return url;
082    }
083
084    public void setUrl(String url) {
085        this.url = url;
086    }
087
088    public String getMethod() {
089        return method;
090    }
091
092    public void setMethod(String method) {
093        this.method = method;
094    }
095
096    public List<Parameter> getHeaders() {
097        return headers;
098    }
099
100    public void setHeaders(List<Parameter> headers) {
101        this.headers = headers;
102    }
103
104    public List<Parameter> getUrlParameters() {
105        return urlParameters;
106    }
107
108    public void setUrlParameters(List<Parameter> urlParameters) {
109        this.urlParameters = urlParameters;
110    }
111
112    public String getBodyType() {
113        return bodyType;
114    }
115
116    public void setBodyType(String bodyType) {
117        this.bodyType = bodyType;
118    }
119
120    public List<Parameter> getFromData() {
121        return fromData;
122    }
123
124    public void setFromData(List<Parameter> fromData) {
125        this.fromData = fromData;
126    }
127
128    public List<Parameter> getFromUrlencoded() {
129        return fromUrlencoded;
130    }
131
132    public void setFromUrlencoded(List<Parameter> fromUrlencoded) {
133        this.fromUrlencoded = fromUrlencoded;
134    }
135
136    public String getBodyJson() {
137        return bodyJson;
138    }
139
140    public void setBodyJson(String bodyJson) {
141        this.bodyJson = bodyJson;
142    }
143
144    public String getRawBody() {
145        return rawBody;
146    }
147
148    public void setRawBody(String rawBody) {
149        this.rawBody = rawBody;
150    }
151
152    @Override
153    protected Map<String, Object> execute(Chain chain) {
154
155        Map<String, Object> urlDataMap = chain.getParameterValues(this, urlParameters);
156        String parametersString = mapToQueryString(urlDataMap);
157        String newUrl = "POST".equalsIgnoreCase(method) || parametersString.isEmpty() ? url : url +
158                (url.contains("?") ? "&" + parametersString : "?" + parametersString);
159
160        Request.Builder reqBuilder = new Request.Builder().url(newUrl);
161
162        Map<String, Object> headersMap = chain.getParameterValues(this, headers);
163        headersMap.forEach((s, o) -> reqBuilder.addHeader(s, String.valueOf(o)));
164
165        if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) {
166            reqBuilder.method("GET", null);
167        } else {
168            reqBuilder.method(method.toUpperCase(), getRequestBody(chain, urlDataMap));
169        }
170
171
172        OkHttpClient okHttpClient = OkHttpClientUtil.buildDefaultClient();
173        try (Response response = okHttpClient.newCall(reqBuilder.build()).execute()) {
174
175            Map<String, Object> result = new HashMap<>();
176            result.put("statusCode", response.code());
177
178            Map<String, String> responseHeaders = new HashMap<>();
179            for (String name : response.headers().names()) {
180                responseHeaders.put(name, response.header(name));
181            }
182            result.put("headers", responseHeaders);
183
184            ResponseBody body = response.body();
185            String bodyString = null;
186            if (body != null) bodyString = body.string();
187
188            if (StringUtil.hasText(bodyString)) {
189                DataType outDataType = null;
190                List<Parameter> outputDefs = getOutputDefs();
191                if (outputDefs != null) {
192                    for (Parameter outputDef : outputDefs) {
193                        if ("body".equalsIgnoreCase(outputDef.getName())) {
194                            outDataType = outputDef.getDataType();
195                            break;
196                        }
197                    }
198                }
199
200                if (outDataType != null && (outDataType == DataType.Object || outDataType
201                        .getValue().startsWith("Array"))) {
202                    result.put("body", JSON.parse(bodyString));
203                } else {
204                    result.put("body", bodyString);
205                }
206            }
207            return result;
208        } catch (IOException e) {
209            throw new RuntimeException(e);
210        }
211    }
212
213    private RequestBody getRequestBody(Chain chain, Map<String, Object> urlDataMap) {
214        if ("json".equals(bodyType)) {
215            Map<String, Object> argsMap = chain.getParameterValues(this);
216            String bodyJsonString = TextPromptTemplate.create(bodyJson).formatToString(argsMap);
217            JSONObject jsonObject = JSON.parseObject(bodyJsonString);
218            jsonObject.putAll(urlDataMap);
219            return RequestBody.create(jsonObject.toString(), MediaType.parse("application/json"));
220        }
221
222        if ("x-www-form-urlencoded".equals(bodyType)) {
223            Map<String, Object> formUrlencodedMap = chain.getParameterValues(this, fromUrlencoded);
224            String bodyString = mapToQueryString(formUrlencodedMap);
225            return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
226        }
227
228        if ("form-data".equals(bodyType)) {
229            Map<String, Object> formDataMap = chain.getParameterValues(this, fromData);
230
231            MultipartBody.Builder builder = new MultipartBody.Builder()
232                    .setType(MultipartBody.FORM);
233
234            formDataMap.forEach((s, o) -> {
235//                if (o instanceof File) {
236//                    File f = (File) o;
237//                    RequestBody body = RequestBody.create(f, MediaType.parse("application/octet-stream"));
238//                    builder.addFormDataPart(s, f.getName(), body);
239//                } else if (o instanceof InputStream) {
240//                    RequestBody body = new HttpClient.InputStreamRequestBody(MediaType.parse("application/octet-stream"), (InputStream) o);
241//                    builder.addFormDataPart(s, s, body);
242//                } else if (o instanceof byte[]) {
243//                    builder.addFormDataPart(s, s, RequestBody.create((byte[]) o));
244//                } else {
245//                    builder.addFormDataPart(s, String.valueOf(o));
246//                }
247                builder.addFormDataPart(s, String.valueOf(o));
248            });
249
250            return builder.build();
251        }
252
253        if ("raw".equals(bodyType)) {
254            Map<String, Object> argsMap = chain.getParameterValues(this);
255            String rawBodyString = TextPromptTemplate.create(rawBody).formatToString(argsMap);
256            return RequestBody.create(rawBodyString, null);
257        }
258        //none
259        return RequestBody.create("", null);
260    }
261
262    @Override
263    public String toString() {
264        return "HttpNode{" +
265                "url='" + url + '\'' +
266                ", method='" + method + '\'' +
267                ", headers=" + headers +
268                ", parameters=" + urlParameters +
269                ", bodyType='" + bodyType + '\'' +
270                ", fromData=" + fromData +
271                ", fromUrlencoded=" + fromUrlencoded +
272                ", bodyJson='" + bodyJson + '\'' +
273                ", rawBody='" + rawBody + '\'' +
274                ", description='" + description + '\'' +
275                ", parameter=" + urlParameters +
276                ", outputDefs=" + outputDefs +
277                ", id='" + id + '\'' +
278                ", name='" + name + '\'' +
279                ", async=" + async +
280                ", inwardEdges=" + inwardEdges +
281                ", outwardEdges=" + outwardEdges +
282                ", condition=" + condition +
283                ", memory=" + memory +
284                ", nodeStatus=" + nodeStatus +
285                '}';
286    }
287}