diff --git a/.gitignore b/.gitignore index 9b1f767..46cb647 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,4 @@ envs/ venv/ environment.yml .DS_Store -Thumbs.db -faster-whisper-large-v3/ \ No newline at end of file +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md index cde9714..20fb3ca 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,14 @@ ## Introduction -ComfyUI-RequestNodes is a custom node plugin for ComfyUI that provides functionality for sending HTTP requests. Currently, it includes the following nodes: +ComfyUI-RequestNodes is a custom node plugin for ComfyUI that provides functionality for sending HTTP requests and related utilities. Currently, it includes the following nodes: -* **Get Node**: Sends GET requests and retrieves responses. -* **Post Node**: Sends POST requests and retrieves responses. -* **Header Node**: Manages HTTP request headers. -* **Key/Value Node**: Creates key/value pairs for building request parameters or headers. +* **Get Request Node**: Sends GET requests and retrieves responses. +* **Post Request Node**: Sends POST requests and retrieves responses. +* **Rest Api Node**: A versatile node for sending various HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) with retry settings. +* **Key/Value Node**: Creates key/value pairs for building request parameters, headers, or other dictionary-like structures. +* **String Replace Node**: Replaces placeholders in a string with provided values. +* **Retry Settings Node**: Creates retry setting configurations for the Rest Api Node. ## Test Resources @@ -42,46 +44,78 @@ To install ComfyUI-RequestNodes, follow these steps: ## Usage -After installation, you can find the "Get Node" and "Post Node" nodes under the "Request Nodes" category in the ComfyUI node list. +After installation, you can find the nodes under the "RequestNode" category in the ComfyUI node list, with subcategories like "Get Request", "Post Request", "REST API", and "Utils". -* **Get Node**: - * **url**: Enter the URL address you want to request. - * **json_array**: Input JSON array generated by Key/Value Nodes as request parameters. - * **headers**: Input request headers generated by Header Node. - * **output_format**: Select the output format of the response, such as "text" or "json". - * **response**: Output response content. +* **Get Request Node**: + * **Category**: RequestNode/Get Request + * **Inputs**: + * `target_url` (STRING, required): The URL to send the GET request to. + * `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node. + * `query_list` (KEY_VALUE, optional): Query parameters, typically from a Key/Value Node. + * **Outputs**: + * `text` (STRING): The response body as text. + * `file` (BYTES): The response body as bytes. + * `json` (JSON): The response body parsed as JSON (if valid). + * `any` (ANY): The raw response content. * ![image](https://github.com/user-attachments/assets/cdb1938f-f8a9-4a4b-a787-90fa4d543523) - -* **Header Node**: - * **json_array**: Input JSON array generated by Key/Value Nodes as request headers. - * **output**: Output merged request headers. - * ![image](https://github.com/user-attachments/assets/832e4a4a-ffd5-42b2-bc3c-0c24f113c0bf) - - -* **Key/Value Node**: - * **key**: Input key name. - * **value**: Input key value. - * **input_json**: Optional, connect output from other Key/Value Nodes. - * **output**: Output JSON array containing the key/value pair. - * ![image](https://github.com/user-attachments/assets/dfe7dab0-2b1b-4f99-ac6f-89e01d03b7e0) - - - - -* **Post Node**: - * **url**: Enter the URL address you want to request. - * **data**: Enter the data for the POST request, in JSON format. - * In the `data` field, you can use placeholders like `__str1__`, `__str2__`, etc. in your JSON request body. - * These placeholders will be replaced by the values of input strings `str1`, `str2`, etc. respectively. - * For example, if you have an input string named `str1` with the value "example", and your JSON data is `{"key": "__str1__"}`, the actual request body will be `{"key": "example"}`. - * **output_format**: Select the output format of the response, such as "text" or "json". - * **response**: Output response content. +* **Post Request Node**: + * **Category**: RequestNode/Post Request + * **Inputs**: + * `target_url` (STRING, required): The URL to send the POST request to. + * `request_body` (STRING, required, multiline): The request body, typically in JSON format. Placeholders like `__str0__`, `__str1__`, ..., `__str9__` can be used and will be replaced by the corresponding optional string inputs. + * `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node. + * `str0` to `str9` (STRING, optional): String inputs to replace placeholders in `request_body`. + * **Outputs**: + * `text` (STRING): The response body as text. + * `file` (BYTES): The response body as bytes. + * `json` (JSON): The response body parsed as JSON (if valid). + * `any` (ANY): The raw response content. * ![image](https://github.com/user-attachments/assets/6eda9fef-48cf-478c-875e-6bd6d850bff2) +* **Rest Api Node**: + * **Category**: RequestNode/REST API + * **Inputs**: + * `target_url` (STRING, required): The URL for the request. + * `method` (Dropdown, required): The HTTP method to use (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS). + * `request_body` (STRING, required, multiline): The request body (hidden for HEAD, OPTIONS, DELETE methods). + * `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node. + * `RETRY_SETTING` (RETRY_SETTING, optional): Retry settings, typically from a Retry Settings Node. + * **Outputs**: + * `text` (STRING): The response body as text. + * `file` (BYTES): The response body as bytes. + * `json` (JSON): The response body parsed as JSON (if valid). For HEAD requests, this output contains the response headers. + * `headers` (DICT): The response headers as a dictionary. + * `status_code` (INT): The HTTP status code of the response. + * `any` (ANY): The raw response content. +* **Key/Value Node**: + * **Category**: RequestNode/KeyValue + * **Inputs**: + * `key` (STRING, required): The key name. + * `value` (STRING, required): The key value. + * `KEY_VALUE` (KEY_VALUE, optional): Connect output from other Key/Value Nodes to merge pairs. + * **Outputs**: + * `KEY_VALUE` (KEY_VALUE): A dictionary containing the key/value pair(s). + * ![image](https://github.com/user-attachments/assets/dfe7dab0-2b1b-4f99-ac6f-89e01d03b7e0) + +* **String Replace Node**: + * **Category**: RequestNode/Utils + * **Inputs**: + * `input_string` (STRING, required, multiline): The string containing placeholders to be replaced. + * `placeholders` (KEY_VALUE, optional): Key/Value pairs where keys are placeholders (e.g., `__my_placeholder__`) and values are the replacement strings. + * **Outputs**: + * `output_string` (STRING): The string with placeholders replaced. + +* **Retry Settings Node**: + * **Category**: RequestNode/KeyValue + * **Inputs**: + * `key` (Dropdown, required): The retry setting key (`max_retry`, `retry_interval`, `retry_until_status_code`, `retry_until_not_status_code`). + * `value` (INT, required): The integer value for the retry setting. + * `RETRY_SETTING` (RETRY_SETTING, optional): Connect output from other Retry Settings Nodes to merge settings. + * **Outputs**: + * `RETRY_SETTING` (RETRY_SETTING): A dictionary containing the retry setting(s). - ## Contribution Welcome to submit issues and pull requests to improve ComfyUI-RequestNodes! diff --git a/README_zh.md b/README_zh.md index c59842d..a6d1e9d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -2,23 +2,25 @@ [English Version](README.md) -## 簡介 +## 介紹 -ComfyUI-RequestNodes 是一個 ComfyUI 的自定義節點插件,提供用於發送 HTTP 請求的功能。目前包含以下節點: +ComfyUI-RequestNodes 是一個用於 ComfyUI 的自訂節點插件,提供了發送 HTTP 請求及相關實用工具的功能。目前,它包含以下節點: -* **Get Node**: 發送 GET 請求並獲取響應。 -* **Post Node**: 發送 POST 請求並獲取響應。 -* **Header Node**: 管理 HTTP 請求頭部。 -* **Key/Value Node**: 創建鍵值對數據,可用於構建請求參數或頭部。 +* **Get Request Node**: 發送 GET 請求並檢索響應。 +* **Post Request Node**: 發送 POST 請求並檢索響應。 +* **Rest Api Node**: 一個多功能的節點,用於發送各種 HTTP 方法 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) 並支援重試設定。 +* **Key/Value Node**: 創建鍵/值對,用於構建請求參數、標頭或其他類似字典的結構。 +* **String Replace Node**: 使用提供的值替換字串中的佔位符。 +* **Retry Settings Node**: 為 Rest Api Node 創建重試設定配置。 ## 測試資源 -本插件包含以下測試資源: -* `base_flask_server.py` - 測試用 Python Flask 服務器 -* `get_node.json` - GET 請求工作流模板 +插件包含以下測試資源: +* `base_flask_server.py` - 用於測試的 Python Flask 伺服器 +* `get_node.json` - GET 請求工作流程模板 ![7da7547075bfc89220dc8bff1f8c62f](https://github.com/user-attachments/assets/ce9e6f04-1618-433a-8d69-4857a280dc61) ![8b717a369b523e69385f50fe176ccf2](https://github.com/user-attachments/assets/b5f1795a-589d-43df-8e82-a1a079262e5f) -* `post_node.json` - POST 請求工作流模板 +* `post_node.json` - POST 請求工作流程模板 ![cd28f656ab8ae165db4095d325fa38d](https://github.com/user-attachments/assets/28ad21f5-2949-4c41-8d61-994b9170f37c) ![c7549268846ea3b570462c92acfb16c](https://github.com/user-attachments/assets/12dbd00b-af85-439d-978f-301760536005) @@ -26,63 +28,102 @@ ComfyUI-RequestNodes 是一個 ComfyUI 的自定義節點插件,提供用於 要安裝 ComfyUI-RequestNodes,請按照以下步驟操作: -1. **打開 ComfyUI custom_nodes 目錄。** - * 在你的 ComfyUI 安裝目錄下,找到 `custom_nodes` 文件夾。 +1. **打開 ComfyUI 的 custom_nodes 目錄。** + * 在您的 ComfyUI 安裝目錄中,找到 `custom_nodes` 資料夾。 -2. **克隆 ComfyUI-RequestNodes 倉庫。** +2. **克隆 ComfyUI-RequestNodes 儲存庫。** * 在 `custom_nodes` 目錄中打開終端或命令提示符。 - * 運行以下命令來克隆倉庫: + * 運行以下命令克隆儲存庫: ```bash git clone https://github.com/felixszeto/ComfyUI-RequestNodes.git ``` -3. **重啟 ComfyUI。** - * 關閉並重新啟動 ComfyUI,以加載新安裝的節點。 +3. **重新啟動 ComfyUI。** + * 關閉並重新啟動 ComfyUI 以載入新安裝的節點。 ## 使用方法 -安裝完成後,你可以在 ComfyUI 的節點列表中找到 "Request Nodes" 類別下的 "Get Node" 和 "Post Node" 節點。 +安裝後,您可以在 ComfyUI 節點列表的 "RequestNode" 分類下找到這些節點,並有 "Get Request", "Post Request", "REST API", 和 "Utils" 等子分類。 -* **Get Node**: - * **url**: 輸入你要請求的 URL 地址。 - * **json_array**: 輸入由Key/Value Node生成的JSON數組作為請求參數。 - * **headers**: 輸入由Header Node生成的請求頭部。 - * **output_format**: 選擇響應的輸出格式,例如 "text" 或 "json"。 - * **response**: 輸出響應內容。 +* **Get Request Node**: + * **分類**: RequestNode/Get Request + * **輸入**: + * `target_url` (STRING, 必需): 要發送 GET 請求的 URL。 + * `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。 + * `query_list` (KEY_VALUE, 可選): 查詢參數,通常來自 Key/Value Node。 + * **輸出**: + * `text` (STRING): 響應主體作為文本。 + * `file` (BYTES): 響應主體作為字節。 + * `json` (JSON): 響應主體解析為 JSON (如果有效)。 + * `any` (ANY): 原始響應內容。 * ![image](https://github.com/user-attachments/assets/cdb1938f-f8a9-4a4b-a787-90fa4d543523) -* **Header Node**: - * **json_array**: 輸入由Key/Value Node生成的JSON數組作為請求頭部。 - * **output**: 輸出合併後的請求頭部。 - * ![image](https://github.com/user-attachments/assets/832e4a4a-ffd5-42b2-bc3c-0c24f113c0bf) +* **Post Request Node**: + * **分類**: RequestNode/Post Request + * **輸入**: + * `target_url` (STRING, 必需): 要發送 POST 請求的 URL。 + * `request_body` (STRING, 必需, 多行): 請求主體,通常為 JSON 格式。可以使用 `__str0__`, `__str1__`, ..., `__str9__` 等佔位符,它們將被對應的可選字串輸入替換。 + * `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。 + * `str0` 到 `str9` (STRING, 可選): 用於替換 `request_body` 中佔位符的字串輸入。 + * **輸出**: + * `text` (STRING): 響應主體作為文本。 + * `file` (BYTES): 響應主體作為字節。 + * `json` (JSON): 響應主體解析為 JSON (如果有效)。 + * `any` (ANY): 原始響應內容。 + * ![image](https://github.com/user-attachments/assets/6eda9fef-48cf-478c-875e-6bd6d850bff2) + +* **Rest Api Node**: + * **分類**: RequestNode/REST API + * **輸入**: + * `target_url` (STRING, 必需): 請求的 URL。 + * `method` (下拉選單, 必需): 要使用的 HTTP 方法 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)。 + * `request_body` (STRING, 必需, 多行): 請求主體 (對於 HEAD, OPTIONS, DELETE 方法會隱藏)。 + * `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。 + * `RETRY_SETTING` (RETRY_SETTING, 可選): 重試設定,通常來自 Retry Settings Node。 + * **輸出**: + * `text` (STRING): 響應主體作為文本。 + * `file` (BYTES): 響應主體作為字節。 + * `json` (JSON): 響應主體解析為 JSON (如果有效)。對於 HEAD 請求,此輸出包含響應標頭。 + * `headers` (DICT): 響應標頭作為字典。 + * `status_code` (INT): 響應的 HTTP 狀態碼。 + * `any` (ANY): 原始響應內容。 * **Key/Value Node**: - * **key**: 輸入鍵名。 - * **value**: 輸入鍵值。 - * **input_json**: 可選,連接其他Key/Value Node的輸出。 - * **output**: 輸出包含當前鍵值對的JSON數組。 + * **分類**: RequestNode/KeyValue + * **輸入**: + * `key` (STRING, 必需): 鍵名。 + * `value` (STRING, 必需): 鍵值。 + * `KEY_VALUE` (KEY_VALUE, 可選): 連接其他 Key/Value Node 的輸出以合併鍵值對。 + * **輸出**: + * `KEY_VALUE` (KEY_VALUE): 包含鍵值對的字典。 * ![image](https://github.com/user-attachments/assets/dfe7dab0-2b1b-4f99-ac6f-89e01d03b7e0) +* **String Replace Node**: + * **分類**: RequestNode/Utils + * **輸入**: + * `input_string` (STRING, 必需, 多行): 包含要替換的佔位符的字串。 + * `placeholders` (KEY_VALUE, 可選): 鍵值對,其中鍵是佔位符 (例如,`__my_placeholder__`),值是替換字串。 + * **輸出**: + * `output_string` (STRING): 替換佔位符後的字串。 -* **Post Node**: - * **url**: 輸入你要請求的 URL 地址。 - * **data**: 輸入 POST 請求的數據,JSON 格式。 - * 在 `data` 字段中,你可以使用佔位符,例如 `__str1__`, `__str2__` 等。 - * 這些佔位符將會被輸入字符串 `str1`, `str2` 等的值替換。 - * 例如,如果你有一個名為 `str1` 的輸入字符串,其值為 "example",並且你的 JSON 數據是 `{"key": "__str1__"}`,則實際的請求體將是 `{"key": "example"}`。 - * **output_format**: 選擇響應的輸出格式,例如 "text" 或 "json"。 - * **response**: 輸出響應內容。 - * ![image](https://github.com/user-attachments/assets/6eda9fef-48cf-478c-875e-6bd6d850bff2) +* **Retry Settings Node**: + * **分類**: RequestNode/KeyValue + * **輸入**: + * `key` (下拉選單, 必需): 重試設定的鍵 (`max_retry`, `retry_interval`, `retry_until_status_code`, `retry_until_not_status_code`)。 + * `value` (INT, 必需): 重試設定的整數值。 + * `RETRY_SETTING` (RETRY_SETTING, 可選): 連接其他 Retry Settings Node 的輸出以合併設定。 + * **輸出**: + * `RETRY_SETTING` (RETRY_SETTING): 包含重試設定的字典。 ## 貢獻 -歡迎提交 issue 和 pull request 來改進 ComfyUI-RequestNodes! +歡迎提交 issues 和 pull requests 來改進 ComfyUI-RequestNodes! --- -**注意:** +**注意:** -* 請確保你的 ComfyUI 環境已正確安裝 Git。 -* 如果你的 ComfyUI 安裝目錄不在默認位置,請根據你的實際情況調整路徑。 -* 如果遇到任何問題,請查看 GitHub 倉庫的 issue 頁面或提交新的 issue。 +* 請確保您的 ComfyUI 環境已正確安裝 Git。 +* 如果您的 ComfyUI 安裝目錄不在默認位置,請根據您的實際情況調整路徑。 +* 如果您遇到任何問題,請查看 GitHub 儲存庫的 issue 頁面或提交新的 issue。 diff --git a/__init__.py b/__init__.py index 12bc260..83f8fdb 100644 --- a/__init__.py +++ b/__init__.py @@ -2,13 +2,17 @@ from .get_node import GetRequestNode from .post_node import PostRequestNode -from .header_node import HeaderNode from .key_value_node import KeyValueNode +from .rest_api_node import RestApiNode +from .string_replace_node import StringReplaceNode +from .retry_setting_node import RetrySettingNode NODE_CLASS_MAPPINGS = { "Get Request Node": GetRequestNode, "Post Request Node": PostRequestNode, - "Header Node": HeaderNode, - "Key/Value Node": KeyValueNode + "Rest Api Node": RestApiNode, + "Key/Value Node": KeyValueNode, + "String Replace Node": StringReplaceNode, + "Retry Settings Node": RetrySettingNode } diff --git a/base_flask_server.py b/base_flask_server.py index da028a1..2a25e57 100644 --- a/base_flask_server.py +++ b/base_flask_server.py @@ -1,40 +1,158 @@ - -from flask import Flask, request, jsonify +from flask import Flask, request, jsonify, Response, send_file import json from flask_cors import CORS from flask_socketio import SocketIO - +import io +import traceback +from werkzeug.exceptions import HTTPException +import random app = Flask(__name__) CORS(app) socketio = SocketIO(app, cors_allowed_origins='*') -@app.route('/', methods=['GET', 'POST']) -def main(): - return "OK" +@app.before_request +def log_request_info(): + print(f"\n--- Received Request ---") + print(f"Method: {request.method}") + print(f"Path: {request.path}") + print(f"Headers: {request.headers}") + if request.method == 'GET': + print(f"Query Parameters: {request.args}") + elif request.method in ['POST', 'PUT', 'PATCH']: + try: + print(f"Request Body (JSON): {request.get_json(silent=True)}") + except: + print(f"Request Body (Form): {request.form}") + else: + print(f"Request Data: {request.data}") + print(f"------------------------\n") + +@app.errorhandler(Exception) +def handle_exception(e): + # 打印详细的错误堆栈信息 + print("发生错误:", str(e)) + print(traceback.format_exc()) + + if isinstance(e, HTTPException): + response = jsonify({ + "error": str(e), + "status_code": e.code, + "description": e.description + }) + response.status_code = e.code + return response + + response = jsonify({ + "error": "服务器内部错误", + "details": str(e) + }) + response.status_code = 500 + return response + +@app.route('/api/echo', methods=['POST', 'PUT', 'PATCH']) +def echo_request_body(): + print(f"Received {request.method} request on /api/echo") + try: + data = request.get_json(silent=True) + if data is None: + data = request.form.to_dict() + + headers_info = { + 'Content-Type': request.headers.get('Content-Type'), + 'User-Agent': request.headers.get('User-Agent'), + 'Accept': request.headers.get('Accept') + } + + response_data = { + "received_method": request.method, + "received_data": data, + "received_headers": headers_info + } + print(f"Echoing data: {response_data}") + return jsonify(response_data) + except Exception as e: + print(f"处理 /api/echo 请求时出错: {str(e)}") + return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500 + +@app.route('/api/status/', methods=['GET']) +def return_status_code(code): + print(f"Received GET request on /api/status/{code}") + try: + if code < 100 or code > 599: + print(f"错误: 无效的状态码: {code}") + return jsonify({"error": f"Invalid status code: {code}"}), 400 + + print(f"Returning response with status code: {code}") + return jsonify({"message": f"Returning with status code {code}"}), code + except Exception as e: + print(f"处理 /api/status/{code} 请求时出错: {str(e)}") + return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500 -@app.route('/node', methods=['GET', 'POST']) -def testNode(): - if request.method == 'POST': - # 打印POST请求的所有参数 - data = request.get_json() - if not data: - data = json.loads(request.data.decode()) - print("\n=== POST请求参数 ===") - print("Headers:", dict(request.headers)) - print("JSON数据:", data) - print("Form数据:", request.form) - print("Files:", request.files) - return jsonify({'message': 'success'}) - elif request.method == 'GET': - # 打印GET请求的所有参数 - print("\n=== GET请求参数 ===") - print("Headers:", dict(request.headers)) - print("查询参数:", request.args) - print("Cookies:", request.cookies) - return jsonify({'message': 'success'}) +@app.route('/api/headers', methods=['GET']) +def return_custom_headers(): + print("Received GET request on /api/headers") + response = jsonify({"message": "This response has custom headers"}) + response.headers['X-Custom-Header'] = 'Custom Value' + response.headers['X-Another-Header'] = 'Another Value' + response.headers['X-Request-Method'] = request.method # Example using request context + print("Returning response with custom headers") + return response + +@app.route('/api/file', methods=['GET']) +def return_test_file(): + print("Received GET request on /api/file") + try: + # Create a simple text file in memory and return it + file_content = b"This is the content of the test file from /api/file." + file_data = io.BytesIO(file_content) + file_data.seek(0) + print("Sending file: test_file.txt") + return send_file( + file_data, + mimetype="text/plain", + as_attachment=True, + download_name="test_file.txt", + headers={'X-File-Source': '/api/file'} + ) + except Exception as e: + print(f"发送文件时出错: {str(e)}") + return jsonify({"error": "发送文件时发生错误", "details": str(e)}), 500 + +@app.route('/api/random_failure', methods=['GET']) +def random_failure_endpoint(): + print("Received GET request on /api/random_failure") + if random.random() < 0.2: + print("Simulating random 500 error") + return jsonify({"error": "Simulated internal server error from /api/random_failure"}), 500 + else: + print("Returning successful response from /api/random_failure") + return jsonify({"message": "Success from /api/random_failure", "status": "ok"}) + +@app.route('/api/request_info', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) +def return_request_info(): + print(f"Received {request.method} request on /api/request_info") + try: + request_info = { + "method": request.method, + "path": request.path, + "headers": dict(request.headers), # Convert headers to dict for JSON serialization + "query_parameters": request.args.to_dict(), + "form_data": request.form.to_dict(), + "json_data": request.get_json(silent=True), # silent=True prevents error if body is not JSON + "raw_data": request.data.decode('utf-8', errors='ignore') # Attempt to decode raw data + } + print("Returning request info as JSON") + return jsonify(request_info) + except Exception as e: + print(f"处理 /api/request_info 请求时出错: {str(e)}") + return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500 + if __name__ == "__main__": - socketio.run(app, debug=True, host='0.0.0.0', port=7788) + print("Starting RestApiNode test server...") + print("API available at http://127.0.0.1:7788/api/*") + print("Root endpoint available at http://127.0.0.1:7788/") + socketio.run(app, debug=True, host='0.0.0.0', port=7788, use_reloader=True) \ No newline at end of file diff --git a/get_node.py b/get_node.py index bb20ad0..e2828c6 100644 --- a/get_node.py +++ b/get_node.py @@ -12,8 +12,8 @@ class GetRequestNode: "target_url": ("STRING", {"default": "https://example.com/api"}), }, "optional": { - "headers": ("DICT", {"default": None}), - "json_array": ("LIST", {"default": []}), + "headers": ("KEY_VALUE", {"default": None}), + "query_list": ("KEY_VALUE", {"default": []}), } } @@ -24,29 +24,17 @@ class GetRequestNode: CATEGORY = "RequestNode/Get Request" - def make_get_request(self, target_url, headers=None, json_array=None): - # 構建參數字典 - params = {} - - # 合併json_array中的所有JSON - if json_array: - for json_obj in json_array: - if isinstance(json_obj, dict): - params.update(json_obj) - - # 設置默認headers + def make_get_request(self, target_url, headers=None, query_list=None): request_headers = {'Content-Type': 'application/json'} if headers: request_headers.update(headers) try: - # 使用構建的參數發送 GET 請求 - response = requests.get(target_url, params=params, headers=request_headers) + response = requests.get(target_url, params=query_list, headers=request_headers) - # 準備四種不同格式的輸出 text_output = response.text - file_output = response.content # 直接返回字節串 + file_output = response.content try: json_output = response.json() @@ -58,7 +46,7 @@ class GetRequestNode: except Exception as e: error_message = str(e) text_output = error_message - file_output = error_message.encode() # 將錯誤消息轉換為字節串 + file_output = error_message.encode() json_output = {"error": error_message} any_output = error_message.encode() @@ -69,5 +57,5 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "GetRequestNode": "GET Request Node" + "GetRequestNode": "Get Request Node" } diff --git a/header_node.py b/header_node.py deleted file mode 100644 index 453edf8..0000000 --- a/header_node.py +++ /dev/null @@ -1,40 +0,0 @@ -import json - -class HeaderNode: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - return { - "required": {}, - "optional": { - "json_array": ("LIST", {"default": []}), - } - } - - RETURN_TYPES = ("DICT",) - RETURN_NAMES = ("headers",) - - FUNCTION = "create_headers" - - CATEGORY = "RequestNode/Header" - - def create_headers(self, json_array=None): - headers = {} - - # 合併json_array中的所有JSON - if json_array: - for json_obj in json_array: - if isinstance(json_obj, dict): - headers.update(json_obj) - - return (headers,) - -NODE_CLASS_MAPPINGS = { - "HeaderNode": HeaderNode -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "HeaderNode": "Header Node" -} \ No newline at end of file diff --git a/key_value_node.py b/key_value_node.py index bfdc9a6..9a5ab2b 100644 --- a/key_value_node.py +++ b/key_value_node.py @@ -1,5 +1,3 @@ -import json - class KeyValueNode: def __init__(self): pass @@ -12,29 +10,27 @@ class KeyValueNode: "value": ("STRING", {"default": ""}), }, "optional": { - "input_json": ("LIST", {"default": None}), + "KEY_VALUE": ("KEY_VALUE", {"default": None}), } } - RETURN_TYPES = ("LIST",) - RETURN_NAMES = ("json_array",) + RETURN_TYPES = ("KEY_VALUE",) + RETURN_NAMES = ("KEY_VALUE",) FUNCTION = "create_key_value" CATEGORY = "RequestNode/KeyValue" - def create_key_value(self, key="", value="", input_json=None): + def create_key_value(self, key="", value="", KEY_VALUE=None): output = {} - # 合并输入JSON (从LIST中取第一个元素) - if input_json and len(input_json) > 0 and isinstance(input_json[0], dict): - output.update(input_json[0]) + if KEY_VALUE is not None: + output.update(KEY_VALUE) - # 添加当前key/value if key and value: output[key] = value - return ([output],) + return (output,) NODE_CLASS_MAPPINGS = { "KeyValueNode": KeyValueNode diff --git a/post_node.py b/post_node.py index e013169..c724561 100644 --- a/post_node.py +++ b/post_node.py @@ -14,7 +14,7 @@ class PostRequestNode: "request_body": ("STRING", {"default": "{}", "multiline": True}), }, "optional": { - "headers": ("DICT", {"default": None}), + "headers": ("KEY_VALUE", {"default": None}), "str0": ("STRING", {"default": ""}), "str1": ("STRING", {"default": ""}), "str2": ("STRING", {"default": ""}), @@ -36,16 +36,14 @@ class PostRequestNode: CATEGORY = "RequestNode/Post Request" def make_post_request(self, target_url, request_body, headers=None, str0="", str1="", str2="", str3="", str4="", str5="", str6="", str7="", str8="", str9=""): - # 收集所有字符串輸入 string_inputs = { "str0": str0, "str1": str1, "str2": str2, "str3": str3, "str4": str4, "str5": str5, "str6": str6, "str7": str7, "str8": str8, "str9": str9 } - # 替換 request_body 中的佔位符 for key, value in string_inputs.items(): placeholder = f"__{key}__" - if value: # 只有當值不為空時才進行替換 + if value: request_body = request_body.replace(placeholder, value) # 嘗試解析 request_body 為 JSON @@ -54,7 +52,6 @@ class PostRequestNode: except json.JSONDecodeError: body_data = {"error": "Invalid JSON in request_body"} - # 合併默認headers與自定義headers request_headers = {'Content-Type': 'application/json'} if headers: request_headers.update(headers) @@ -87,5 +84,5 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "PostRequestNode": "POST Request Node" + "PostRequestNode": "Post Request Node" } diff --git a/rest_api_node.py b/rest_api_node.py new file mode 100644 index 0000000..c38a806 --- /dev/null +++ b/rest_api_node.py @@ -0,0 +1,195 @@ +import requests +import json +import io + +class RestApiNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "target_url": ("STRING", {"default": "https://example.com/api", "forceInput":True}), + "request_body": ("STRING", {"default": "{}", "multiline": True, "forceInput":True}), + "method": (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], {"default": "GET"}), + }, + "optional": { + "headers": ("KEY_VALUE", {"default": None}), + "RETRY_SETTING": ("RETRY_SETTING", {"default": None}), + } + } + + @classmethod + def HIDE_INPUTS(s, method): + hide_inputs = {} + + if method in ["HEAD", "OPTIONS", "DELETE"]: + hide_inputs["request_body"] = True + + return hide_inputs + + RETURN_TYPES = ("STRING", "BYTES", "JSON", "DICT", "INT", "ANY") + RETURN_NAMES = ("text", "file", "json", "headers", "status_code", "any") + + FUNCTION = "make_request" + + CATEGORY = "RequestNode/REST API" + + def make_request(self, target_url, method, request_body, headers=None, RETRY_SETTING=None): + + request_headers = {'Content-Type': 'application/json'} + if headers: + request_headers.update(headers) + + default_max_retry = 3 + max_retry = default_max_retry + retry_until_status_code = 0 + retry_until_not_status_code = 0 + retry_interval = 1000 + retry_non_2xx = False + + if RETRY_SETTING is not None: + if "max_retry" in RETRY_SETTING: + max_retry = RETRY_SETTING["max_retry"] + retry_until_status_code = RETRY_SETTING.get("retry_until_status_code", 0) + retry_until_not_status_code = RETRY_SETTING.get("retry_until_not_status_code", 0) + retry_interval = RETRY_SETTING.get("retry_interval", 1000) + + if "max_retry" in RETRY_SETTING and not any(key in RETRY_SETTING for key in ["retry_until_status_code", "retry_until_not_status_code"]): + retry_non_2xx = True + + params = {} + body_data = None + + if method in ["POST", "PUT", "PATCH"]: + try: + body_data = json.loads(request_body) + except json.JSONDecodeError: + body_data = {"error": "Invalid JSON in request_body"} + elif method == "GET": + try: + params_data = json.loads(request_body) + if isinstance(params_data, dict): + params = params_data + except json.JSONDecodeError: + pass + + def send_request(): + if method == "GET": + return requests.get(target_url, params=params, headers=request_headers) + elif method == "POST": + return requests.post(target_url, json=body_data, headers=request_headers) + elif method == "PUT": + return requests.put(target_url, json=body_data, headers=request_headers) + elif method == "DELETE": + return requests.delete(target_url, headers=request_headers) + elif method == "PATCH": + return requests.patch(target_url, json=body_data, headers=request_headers) + elif method == "HEAD": + return requests.head(target_url, headers=request_headers) + elif method == "OPTIONS": + return requests.options(target_url, headers=request_headers) + else: + raise ValueError(f"Unsupported HTTP method: {method}") + + max_attempts = 1 + attempts = 0 + last_exception = None + + has_specific_retry_conditions = (retry_until_status_code > 0 or retry_until_not_status_code > 0 or retry_non_2xx) + + if has_specific_retry_conditions and max_retry == 0: + import sys + max_attempts = sys.maxsize + elif max_retry > 0: + max_attempts = max_retry + 1 + else: + max_attempts = 1 + + try: + while attempts < max_attempts: + try: + attempts += 1 + response = send_request() + + if retry_until_status_code > 0 and response.status_code != retry_until_status_code: + if attempts < max_attempts: + import time + time.sleep(retry_interval / 1000) + continue + elif retry_until_not_status_code > 0 and response.status_code == retry_until_not_status_code: + if attempts < max_attempts: + import time + time.sleep(retry_interval / 1000) + continue + elif retry_non_2xx and (response.status_code < 200 or response.status_code >= 300): + if attempts < max_attempts: + import time + time.sleep(retry_interval / 1000) + continue + + break + + except Exception as e: + last_exception = e + if attempts < max_attempts: + import time + time.sleep(retry_interval / 1000) + else: + raise + + text_output = response.text + + if method == "HEAD": + file_output = io.BytesIO(b"") + any_output = b"" + else: + file_output = io.BytesIO(response.content) + any_output = response.content + + try: + if method == "HEAD": + json_output = dict(response.headers) + else: + json_output = response.json() + except json.JSONDecodeError: + if method == "HEAD": + json_output = dict(response.headers) + else: + json_output = {"error": "Response is not valid JSON"} + + if attempts > 1: + retry_info = {"retries": attempts - 1} + if max_retry > 0: + retry_info["max_retry"] = max_retry + if retry_until_status_code > 0: + retry_info["retry_until_status_code"] = retry_until_status_code + if retry_until_not_status_code > 0: + retry_info["retry_until_not_status_code"] = retry_until_not_status_code + if retry_non_2xx: + retry_info["retry_non_2xx"] = True + if isinstance(json_output, dict): + json_output["retry_info"] = retry_info + + except Exception as e: + error_message = str(e) + text_output = error_message + file_output = io.BytesIO(error_message.encode()) + json_output = {"error": error_message} + headers_output = {"error": error_message} + status_code = 0 + any_output = error_message + else: + headers_output = dict(response.headers) + status_code = response.status_code + + return (text_output, file_output, json_output, headers_output, status_code, any_output) + +NODE_CLASS_MAPPINGS = { + "RestApiNode": RestApiNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "RestApiNode": "Rest Api Node" +} \ No newline at end of file diff --git a/retry_setting_node.py b/retry_setting_node.py new file mode 100644 index 0000000..0da9cd5 --- /dev/null +++ b/retry_setting_node.py @@ -0,0 +1,41 @@ +class RetrySettingNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "key": (["max_retry", "retry_interval", "retry_until_status_code", "retry_until_not_status_code"], {"default": ""}, {"default": "max_retry"}), + "value": ("INT", {"default": 3, "min": 0, "max": 99999}), + }, + "optional": { + "RETRY_SETTING": ("RETRY_SETTING", {"default": None}), + } + } + + RETURN_TYPES = ("RETRY_SETTING",) + RETURN_NAMES = ("RETRY_SETTING",) + + FUNCTION = "create_retry_setting" + + CATEGORY = "RequestNode/KeyValue" + + def create_retry_setting(self, key="", value="", RETRY_SETTING=None): + output = {} + + if RETRY_SETTING is not None: + output.update(RETRY_SETTING) + + if key and value: + output[key] = value + + return (output,) + +NODE_CLASS_MAPPINGS = { + "RetrySettingNode": RetrySettingNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "RetrySettingNode": "Retry Settings Node" +} \ No newline at end of file diff --git a/string_replace_node.py b/string_replace_node.py new file mode 100644 index 0000000..57b634e --- /dev/null +++ b/string_replace_node.py @@ -0,0 +1,41 @@ +class StringReplaceNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_string": ("STRING", {"default": "", "multiline": True}), + }, + "optional": { + "placeholders": ("KEY_VALUE", {"default": None}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("output_string",) + + FUNCTION = "replace_string" + + CATEGORY = "RequestNode/Utils" + + def replace_string(self, input_string, placeholders=None): + if not placeholders: + return (input_string,) + + output_string = input_string + + for key, value in placeholders.items(): + str_value = str(value) + output_string = output_string.replace(key, str_value) + + return (output_string,) + +NODE_CLASS_MAPPINGS = { + "StringReplaceNode": StringReplaceNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "StringReplaceNode": "String Replace Node" +} \ No newline at end of file