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:
@@ -70,6 +70,8 @@ def require_api_key(
|
||||
})
|
||||
raise BusinessException("API Key 无效或已过期", BizCode.API_KEY_INVALID)
|
||||
|
||||
ApiKeyAuthService.check_app_published(db, api_key_obj)
|
||||
|
||||
if scopes:
|
||||
missing_scopes = []
|
||||
for scope in scopes:
|
||||
|
||||
@@ -66,6 +66,7 @@ class BizCode(IntEnum):
|
||||
PERMISSION_DENIED = 6010
|
||||
INVALID_CONVERSATION = 6011
|
||||
CONFIG_MISSING = 6012
|
||||
APP_NOT_PUBLISHED = 6013
|
||||
|
||||
# 模型(7xxx)
|
||||
MODEL_CONFIG_INVALID = 7001
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
|
||||
@@ -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(
|
||||
# ...
|
||||
# )
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user