light-heart-labs--ods
141 行
5.5 KiB
JSON
141 行
5.5 KiB
JSON
{
|
|
"name": "Langflow Flow API",
|
|
"active": true,
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "langflow-run",
|
|
"options": {}
|
|
},
|
|
"id": "langflow-webhook",
|
|
"name": "Langflow Webhook",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
250,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const body = $json.body || $json;\nconst headers = $json.headers || {};\n\nconst expectedKey = $env.LANGFLOW_API_KEY;\nconst providedKey = headers['x-api-key'] || headers['X-API-Key'];\n\n// Reject keys exceeding max length to prevent truncation attacks\nconst MAX_KEY_LENGTH = 128;\n\nfunction safeCompare(a, b) {\n // Reject keys exceeding max length first (length is not secret)\n if (a.length > MAX_KEY_LENGTH || b.length > MAX_KEY_LENGTH) {\n return false;\n }\n\n // Constant-time comparison: always iterate MAX_KEY_LENGTH times\n // to prevent timing attacks that could leak the expected key length\n let result = a.length ^ b.length; // Non-zero if lengths differ\n\n for (let i = 0; i < MAX_KEY_LENGTH; i++) {\n // When index exceeds string length, use 0 (null byte) to avoid out-of-bounds\n const charA = i < a.length ? a.charCodeAt(i) : 0;\n const charB = i < b.length ? b.charCodeAt(i) : 0;\n result |= charA ^ charB;\n }\n return result === 0;\n}\n\nif (expectedKey && expectedKey.length > 0) {\n if (!providedKey) {\n return [{ json: { error: 'Authentication required', code: 401 } }];\n }\n if (!safeCompare(expectedKey, providedKey)) {\n return [{ json: { error: 'Invalid API key', code: 403 } }];\n }\n}\n\nconst flowId = body.flowId || $env.LANGFLOW_DEFAULT_FLOW_ID;\nif (!flowId) {\n return [{ json: { error: 'flowId required', code: 400 } }];\n}\n\n// Validate flowId format (UUID v4 or alphanumeric with hyphens only)\nconst validIdPattern = /^[a-zA-Z0-9-]+$/;\nif (!validIdPattern.test(flowId)) {\n return [{ json: { error: 'Invalid flowId format', code: 400 } }];\n}\n\nreturn [{\n json: {\n input_value: body.input || body.message || body.prompt || 'Hello',\n flowId: flowId,\n input_type: body.input_type || 'chat',\n output_type: body.output_type || 'chat',\n tweaks: body.tweaks || {}\n }\n}];"
|
|
},
|
|
"id": "validate-request",
|
|
"name": "Validate Request",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
450,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "={{ ($env.LANGFLOW_INTERNAL_URL || 'http://langflow:7860') + '/api/v1/run/' + encodeURIComponent($json.flowId) }}",
|
|
"method": "POST",
|
|
"headers": {
|
|
"list": [
|
|
{
|
|
"name": "Content-Type",
|
|
"value": "application/json"
|
|
}
|
|
]
|
|
},
|
|
"body": "={{ JSON.stringify({\n input_value: $json.input_value,\n input_type: $json.input_type,\n output_type: $json.output_type,\n tweaks: $json.tweaks\n}) }}",
|
|
"options": {
|
|
"timeout": 120000,
|
|
"continueOnFail": true
|
|
}
|
|
},
|
|
"id": "langflow-api",
|
|
"name": "Langflow API",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
650,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const response = $json;\n\n// Handle HTTP errors (when continueOnFail catches them)\nif ($('Langflow API').last().error) {\n return [{\n json: {\n error: 'Upstream service error',\n code: 502\n }\n }];\n}\n\nif (response.error) {\n return [{\n json: {\n error: 'Request processing failed',\n code: response.code || 500\n }\n }];\n}\n\n// Extract outputs from Langflow response\nconst outputs = response.outputs || [];\nconst result = outputs[0]?.outputs?.[0]?.results?.message?.text ||\n outputs[0]?.outputs?.[0]?.artifacts?.message ||\n 'No response';\n\nreturn [{\n json: {\n text: result,\n session_id: response.session_id,\n outputs: outputs.map(o => ({\n component: o.component_id,\n type: o.type\n }))\n }\n}];"
|
|
},
|
|
"id": "format-response",
|
|
"name": "Format Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
850,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"respondWith": "json",
|
|
"json": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"id": "respond-to-webhook",
|
|
"name": "Respond to Webhook",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
1050,
|
|
300
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"Langflow Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Validate Request",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Validate Request": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Langflow API",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Langflow API": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Respond to Webhook",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"staticData": null,
|
|
"tags": []
|
|
}
|