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 dev.tinyflow.core.file.FileStorage;
028import okhttp3.*;
029
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.UnsupportedEncodingException;
033import java.net.URLEncoder;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037
038public class HttpNode extends BaseNode {
039
040    private String url;
041    private String method;
042
043    private List<Parameter> headers;
044
045    private String bodyType;
046    private List<Parameter> formData;
047    private List<Parameter> formUrlencoded;
048    private String bodyJson;
049    private String rawBody;
050    private FileStorage fileStorage;
051
052    public static String mapToQueryString(Map<String, Object> map) {
053        if (map == null || map.isEmpty()) {
054            return "";
055        }
056
057        StringBuilder stringBuilder = new StringBuilder();
058
059        for (String key : map.keySet()) {
060            if (StringUtil.noText(key)) {
061                continue;
062            }
063            if (stringBuilder.length() > 0) {
064                stringBuilder.append("&");
065            }
066            stringBuilder.append(key.trim());
067            stringBuilder.append("=");
068            Object value = map.get(key);
069            stringBuilder.append(value == null ? "" : urlEncode(value.toString().trim()));
070        }
071        return stringBuilder.toString();
072    }
073
074    public static String urlEncode(String string) {
075        try {
076            return URLEncoder.encode(string, "UTF-8");
077        } catch (UnsupportedEncodingException e) {
078            throw new RuntimeException(e);
079        }
080    }
081
082    public String getUrl() {
083        return url;
084    }
085
086    public void setUrl(String url) {
087        this.url = url;
088    }
089
090    public String getMethod() {
091        return method;
092    }
093
094    public void setMethod(String method) {
095        this.method = method;
096    }
097
098    public List<Parameter> getHeaders() {
099        return headers;
100    }
101
102    public void setHeaders(List<Parameter> headers) {
103        this.headers = headers;
104    }
105
106    public String getBodyType() {
107        return bodyType;
108    }
109
110    public void setBodyType(String bodyType) {
111        this.bodyType = bodyType;
112    }
113
114    public List<Parameter> getFormData() {
115        return formData;
116    }
117
118    public void setFormData(List<Parameter> formData) {
119        this.formData = formData;
120    }
121
122    public List<Parameter> getFormUrlencoded() {
123        return formUrlencoded;
124    }
125
126    public void setFormUrlencoded(List<Parameter> formUrlencoded) {
127        this.formUrlencoded = formUrlencoded;
128    }
129
130    public String getBodyJson() {
131        return bodyJson;
132    }
133
134    public void setBodyJson(String bodyJson) {
135        this.bodyJson = bodyJson;
136    }
137
138    public String getRawBody() {
139        return rawBody;
140    }
141
142    public void setRawBody(String rawBody) {
143        this.rawBody = rawBody;
144    }
145
146    public FileStorage getFileStorage() {
147        return fileStorage;
148    }
149
150    public void setFileStorage(FileStorage fileStorage) {
151        this.fileStorage = fileStorage;
152    }
153
154    @Override
155    protected Map<String, Object> execute(Chain chain) {
156
157        Map<String, Object> argsMap = chain.getParameterValues(this);
158        String newUrl = TextPromptTemplate.of(url).formatToString(argsMap);
159
160        Request.Builder reqBuilder = new Request.Builder().url(newUrl);
161
162        Map<String, Object> headersMap = chain.getParameterValues(this, headers, argsMap);
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, argsMap));
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            Headers headers = response.headers();
180            for (String name : headers.names()) {
181                responseHeaders.put(name, response.header(name));
182            }
183            result.put("headers", responseHeaders);
184
185            ResponseBody body = response.body();
186            if (body == null) {
187                result.put("body", null);
188                return result;
189            }
190
191            DataType bodyDataType = null;
192            List<Parameter> outputDefs = getOutputDefs();
193            if (outputDefs != null) {
194                for (Parameter outputDef : outputDefs) {
195                    if ("body".equalsIgnoreCase(outputDef.getName())) {
196                        bodyDataType = outputDef.getDataType();
197                        break;
198                    }
199                }
200            }
201
202            if (bodyDataType == null) {
203                result.put("body", body.string());
204            } else {
205                if (bodyDataType == DataType.Object || bodyDataType.getValue().startsWith("Array")) {
206                    String bodyString = body.string();
207                    try {
208                        result.put("body", JSON.parse(bodyString));
209                    } catch (Exception e) {
210                        throw new RuntimeException("can not parse json: " + bodyString, e);
211                    }
212                } else if (bodyDataType == DataType.File) {
213                    try (InputStream stream = body.byteStream()) {
214                        String fileUrl = fileStorage.saveFile(stream, responseHeaders);
215                        result.put("body", fileUrl);
216                    }
217                } else {
218                    result.put("body", body.string());
219                }
220            }
221            return result;
222        } catch (IOException e) {
223            throw new RuntimeException(e);
224        }
225    }
226
227    private RequestBody getRequestBody(Chain chain, Map<String, Object> formatArgs) {
228        if ("json".equals(bodyType)) {
229            String bodyJsonString = TextPromptTemplate.of(bodyJson).formatToString(formatArgs, true);
230            JSONObject jsonObject = JSON.parseObject(bodyJsonString);
231            return RequestBody.create(jsonObject.toString(), MediaType.parse("application/json"));
232        }
233
234        if ("x-www-form-urlencoded".equals(bodyType)) {
235            Map<String, Object> formUrlencodedMap = chain.getParameterValues(this, formUrlencoded);
236            String bodyString = mapToQueryString(formUrlencodedMap);
237            return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
238        }
239
240        if ("form-data".equals(bodyType)) {
241            Map<String, Object> formDataMap = chain.getParameterValues(this, formData, formatArgs);
242
243            MultipartBody.Builder builder = new MultipartBody.Builder()
244                    .setType(MultipartBody.FORM);
245
246            formDataMap.forEach((s, o) -> {
247//                if (o instanceof File) {
248//                    File f = (File) o;
249//                    RequestBody body = RequestBody.create(f, MediaType.parse("application/octet-stream"));
250//                    builder.addFormDataPart(s, f.getName(), body);
251//                } else if (o instanceof InputStream) {
252//                    RequestBody body = new HttpClient.InputStreamRequestBody(MediaType.parse("application/octet-stream"), (InputStream) o);
253//                    builder.addFormDataPart(s, s, body);
254//                } else if (o instanceof byte[]) {
255//                    builder.addFormDataPart(s, s, RequestBody.create((byte[]) o));
256//                } else {
257//                    builder.addFormDataPart(s, String.valueOf(o));
258//                }
259                builder.addFormDataPart(s, String.valueOf(o));
260            });
261
262            return builder.build();
263        }
264
265        if ("raw".equals(bodyType)) {
266            String rawBodyString = TextPromptTemplate.of(rawBody).formatToString(formatArgs);
267            return RequestBody.create(rawBodyString, null);
268        }
269        //none
270        return RequestBody.create("", null);
271    }
272
273    @Override
274    public String toString() {
275        return "HttpNode{" +
276                "url='" + url + '\'' +
277                ", method='" + method + '\'' +
278                ", headers=" + headers +
279                ", bodyType='" + bodyType + '\'' +
280                ", fromData=" + formData +
281                ", fromUrlencoded=" + formUrlencoded +
282                ", bodyJson='" + bodyJson + '\'' +
283                ", rawBody='" + rawBody + '\'' +
284                ", parameters=" + parameters +
285                ", outputDefs=" + outputDefs +
286                ", id='" + id + '\'' +
287                ", name='" + name + '\'' +
288                ", description='" + description + '\'' +
289                ", async=" + async +
290                ", inwardEdges=" + inwardEdges +
291                ", outwardEdges=" + outwardEdges +
292                ", condition=" + condition +
293                ", memory=" + memory +
294                ", nodeStatus=" + nodeStatus +
295                '}';
296    }
297}