feat(workflow): enhance HTTP request node with curl debugging support

- Augment HTTP request node capabilities and add generated curl commands for easier debugging.

feat(log): implement workflow execution logs and search functionality

- Add detailed logging for workflow node execution and enable search capabilities within application logs.

feat(auth): introduce middleware to verify application publication status

- Add a check to ensure the application is published before allowing access.

fix(converter): rectify variable handling logic in Dify converter

- Correct issues related to processing variables within the Dify converter module.

refactor(model): remove quota check decorator from model update operations

- Decouple quota validation from the model update process to streamline the logic.
This commit is contained in:
wwq
2026-04-23 15:46:12 +08:00
parent 09393b2326
commit 404ce9f9ba
13 changed files with 305 additions and 35 deletions

View File

@@ -155,8 +155,13 @@ class DifyConverter(BaseConverter):
def replacer(match: re.Match) -> str:
raw_name = match.group(1)
new_name = self.process_var_selector(raw_name)
return f"{{{{{new_name}}}}}"
try:
new_name = self.process_var_selector(raw_name)
if not new_name:
return match.group(0)
return f"{{{{{new_name}}}}}"
except Exception:
return match.group(0)
return pattern.sub(replacer, content)
@@ -600,8 +605,15 @@ class DifyConverter(BaseConverter):
] = self.trans_variable_format(content["value"])
else:
if node_data["body"]["data"]:
body_content = (node_data["body"]["data"][0].get("value") or
self._process_list_variable_literal(node_data["body"]["data"][0].get("file")))
data_entry = node_data["body"]["data"][0]
body_content = data_entry.get("value")
if not body_content and data_entry.get("file"):
body_content = self._process_list_variable_literal(data_entry.get("file"))
if not body_content:
body_content = ""
elif isinstance(body_content, str):
# Convert session variable format for JSON body
body_content = self.trans_variable_format(body_content)
else:
body_content = ""

View File

@@ -132,11 +132,21 @@ class HttpErrorDefaultTemplate(BaseModel):
description="Default HTTP headers returned on error",
)
files: list = Field(
default_factory=list,
description="Default files list returned on error",
)
output: str = Field(
default="SUCCESS",
description="HTTP response body",
)
curl: str = Field(
default="",
description="Default curl command returned on error",
)
class HttpErrorHandleConfig(BaseModel):
method: HttpErrorHandle = Field(
@@ -272,6 +282,11 @@ class HttpRequestNodeOutput(BaseModel):
description="HTTP response body",
)
curl: str = Field(
default="",
description="Equivalent curl command for the HTTP request",
)
# files: list[File] = Field(
# ...
# )

View File

@@ -167,7 +167,8 @@ class HttpRequestNode(BaseNode):
"status_code": VariableType.NUMBER,
"headers": VariableType.OBJECT,
"files": VariableType.ARRAY_FILE,
"output": VariableType.STRING
"output": VariableType.STRING,
"curl": VariableType.STRING
}
def _build_timeout(self) -> Timeout:
@@ -255,9 +256,13 @@ class HttpRequestNode(BaseNode):
case HttpContentType.NONE:
return {}
case HttpContentType.JSON:
content["json"] = json.loads(self._render_template(
rendered_body = self._render_template(
self.typed_config.body.data, variable_pool
))
).strip()
if not rendered_body:
content["json"] = {}
else:
content["json"] = json.loads(rendered_body)
case HttpContentType.FROM_DATA:
data = {}
files = []
@@ -325,6 +330,65 @@ class HttpRequestNode(BaseNode):
case _:
raise RuntimeError(f"HttpRequest method not supported: {self.typed_config.method}")
def _generate_curl_command(
self,
variable_pool: VariablePool,
url: str,
headers: dict[str, str],
params: dict[str, str],
content: dict[str, Any]
) -> str:
"""
Generate equivalent curl command for debugging.
Args:
variable_pool: Variable Pool
url: Rendered URL
headers: Request headers
params: Query parameters
content: Request body content
Returns:
Curl command string
"""
# Start with curl command
curl_parts = ["curl"]
# Add HTTP method
method = self.typed_config.method.value
if method != "GET":
curl_parts.append(f"-X {method}")
# Add URL with query parameters
if params:
param_str = "&".join([f"{k}={v}" for k, v in params.items()])
full_url = f"{url}?{param_str}" if "?" not in url else f"{url}&{param_str}"
else:
full_url = url
curl_parts.append(f"'{full_url}'")
# Add headers
for key, value in headers.items():
curl_parts.append(f"-H '{key}: {value}'")
# Add body based on content type
if "json" in content:
json_body = json.dumps(content["json"], ensure_ascii=False)
curl_parts.append(f"-d '{json_body}'")
elif "data" in content and "files" not in content:
# Form data
if isinstance(content["data"], dict):
for key, value in content["data"].items():
curl_parts.append(f"-F '{key}={value}'")
elif "content" in content:
# Raw content
curl_parts.append(f"-d '{content['content']}'")
elif "files" in content:
curl_parts.append("# Note: This request includes file uploads")
return " \\\n ".join(curl_parts)
async def execute(self, state: WorkflowState, variable_pool: VariablePool) -> dict | str:
"""
Execute the HTTP request node.
@@ -343,11 +407,22 @@ class HttpRequestNode(BaseNode):
- str: Branch identifier (e.g. "ERROR") when branching is enabled
"""
self.typed_config = HttpRequestNodeConfig(**self.config)
# Build request components
headers = self._build_header(variable_pool) | self._build_auth(variable_pool)
params = self._build_params(variable_pool)
content = await self._build_content(variable_pool)
url = self._render_template(self.typed_config.url, variable_pool)
# Generate curl command for debugging
curl_command = self._generate_curl_command(variable_pool, url, headers, params, content)
logger.info(f"Node {self.node_id}: Generated curl command:\n{curl_command}")
async with httpx.AsyncClient(
verify=self.typed_config.verify_ssl,
timeout=self._build_timeout(),
headers=self._build_header(variable_pool) | self._build_auth(variable_pool),
params=self._build_params(variable_pool),
headers=headers,
params=params,
follow_redirects=True
) as client:
retries = self.typed_config.retry.max_attempts
@@ -355,8 +430,8 @@ class HttpRequestNode(BaseNode):
try:
request_func = self._get_client_method(client)
resp = await request_func(
url=self._render_template(self.typed_config.url, variable_pool),
**(await self._build_content(variable_pool))
url=url,
**content
)
resp.raise_for_status()
logger.info(f"Node {self.node_id}: HTTP request succeeded")
@@ -365,7 +440,8 @@ class HttpRequestNode(BaseNode):
body=response.body,
status_code=resp.status_code,
headers=resp.headers,
files=response.files
files=response.files,
curl=curl_command
).model_dump()
except (httpx.HTTPStatusError, httpx.RequestError) as e:
logger.error(f"HTTP request node exception: {e}")
@@ -382,10 +458,20 @@ class HttpRequestNode(BaseNode):
logger.warning(
f"Node {self.node_id}: HTTP request failed, returning default result"
)
return self.typed_config.error_handle.default.model_dump()
# Update curl command in default error template
error_result = self.typed_config.error_handle.default.model_dump()
error_result["curl"] = curl_command
return error_result
case HttpErrorHandle.BRANCH:
logger.warning(
f"Node {self.node_id}: HTTP request failed, switching to error handling branch"
)
return {"output": "ERROR"}
return {
"output": "ERROR",
"body": "",
"status_code": 500,
"headers": {},
"files": [],
"curl": curl_command
}
raise RuntimeError("http request failed")