mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2026-08-11 22:30:22 +02:00
feat: Respond to chat and wait for response (#12546)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
co-authored by
कारतोफ्फेलस्क्रिप्ट™
Shireen Missi
parent
e61b25c53f
commit
a98ed2ca49
@@ -14,7 +14,12 @@ import type {
|
||||
IRunExecutionData,
|
||||
IExecuteData,
|
||||
} from 'n8n-workflow';
|
||||
import { createDeferredPromise, FORM_NODE_TYPE, WAIT_NODE_TYPE } from 'n8n-workflow';
|
||||
import {
|
||||
createDeferredPromise,
|
||||
FORM_NODE_TYPE,
|
||||
WAIT_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
} from 'n8n-workflow';
|
||||
import type { Readable } from 'stream';
|
||||
import { finished } from 'stream/promises';
|
||||
|
||||
@@ -23,6 +28,7 @@ import {
|
||||
handleFormRedirectionCase,
|
||||
setupResponseNodePromise,
|
||||
prepareExecutionData,
|
||||
handleHostedChatResponse,
|
||||
} from '../webhook-helpers';
|
||||
import type { IWebhookResponseCallbackData } from '../webhook.types';
|
||||
|
||||
@@ -38,6 +44,15 @@ describe('autoDetectResponseMode', () => {
|
||||
workflow.nodes = {};
|
||||
});
|
||||
|
||||
test('should return hostedChat when start node is CHAT_TRIGGER_NODE_TYPE, method is POST, and public is true', () => {
|
||||
const workflowStartNode = mock<INode>({
|
||||
type: CHAT_TRIGGER_NODE_TYPE,
|
||||
parameters: { options: { responseMode: 'responseNodes' } },
|
||||
});
|
||||
const result = autoDetectResponseMode(workflowStartNode, workflow, 'POST');
|
||||
expect(result).toBe('hostedChat');
|
||||
});
|
||||
|
||||
test('should return undefined if start node is WAIT_NODE_TYPE with resume not equal to form', () => {
|
||||
const workflowStartNode = mock<INode>({
|
||||
type: WAIT_NODE_TYPE,
|
||||
@@ -259,6 +274,61 @@ describe('setupResponseNodePromise', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleHostedChatResponse', () => {
|
||||
it('should send executionStarted: true and executionId when responseMode is hostedChat and didSendResponse is false', async () => {
|
||||
const res = {
|
||||
send: jest.fn(),
|
||||
end: jest.fn(),
|
||||
} as unknown as express.Response;
|
||||
const executionId = 'testExecutionId';
|
||||
let didSendResponse = false;
|
||||
const responseMode = 'hostedChat';
|
||||
|
||||
(res.send as jest.Mock).mockImplementation((data) => {
|
||||
expect(data).toEqual({ executionStarted: true, executionId });
|
||||
});
|
||||
|
||||
const result = handleHostedChatResponse(res, responseMode, didSendResponse, executionId);
|
||||
|
||||
expect(res.send).toHaveBeenCalled();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(res.end).toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should not send response when responseMode is not hostedChat', () => {
|
||||
const res = {
|
||||
send: jest.fn(),
|
||||
end: jest.fn(),
|
||||
} as unknown as express.Response;
|
||||
const executionId = 'testExecutionId';
|
||||
let didSendResponse = false;
|
||||
const responseMode = 'responseNode';
|
||||
|
||||
const result = handleHostedChatResponse(res, responseMode, didSendResponse, executionId);
|
||||
|
||||
expect(res.send).not.toHaveBeenCalled();
|
||||
expect(res.end).not.toHaveBeenCalled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not send response when didSendResponse is true', () => {
|
||||
const res = {
|
||||
send: jest.fn(),
|
||||
end: jest.fn(),
|
||||
} as unknown as express.Response;
|
||||
const executionId = 'testExecutionId';
|
||||
let didSendResponse = true;
|
||||
const responseMode = 'hostedChat';
|
||||
|
||||
const result = handleHostedChatResponse(res, responseMode, didSendResponse, executionId);
|
||||
|
||||
expect(res.send).not.toHaveBeenCalled();
|
||||
expect(res.end).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareExecutionData', () => {
|
||||
const workflowStartNode = mock<INode>({ name: 'Start' });
|
||||
const webhookResultData: IWebhookResponseData = {
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
WebhookResponseData,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
createDeferredPromise,
|
||||
ExecutionCancelledError,
|
||||
FORM_NODE_TYPE,
|
||||
@@ -70,6 +71,21 @@ import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-da
|
||||
import * as WorkflowHelpers from '@/workflow-helpers';
|
||||
import { WorkflowRunner } from '@/workflow-runner';
|
||||
|
||||
export function handleHostedChatResponse(
|
||||
res: express.Response,
|
||||
responseMode: WebhookResponseMode,
|
||||
didSendResponse: boolean,
|
||||
executionId: string,
|
||||
): boolean {
|
||||
if (responseMode === 'hostedChat' && !didSendResponse) {
|
||||
res.send({ executionStarted: true, executionId });
|
||||
process.nextTick(() => res.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
return didSendResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the webhooks which should be created for the given workflow
|
||||
*/
|
||||
@@ -111,6 +127,23 @@ export function getWorkflowWebhooks(
|
||||
return returnData;
|
||||
}
|
||||
|
||||
const getChatResponseMode = (workflowStartNode: INode, method: string) => {
|
||||
const parameters = workflowStartNode.parameters as {
|
||||
public: boolean;
|
||||
options?: { responseMode: string };
|
||||
};
|
||||
|
||||
if (workflowStartNode.type !== CHAT_TRIGGER_NODE_TYPE) return undefined;
|
||||
|
||||
if (method === 'GET') return 'onReceived';
|
||||
|
||||
if (method === 'POST' && parameters.options?.responseMode === 'responseNodes') {
|
||||
return 'hostedChat';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export function autoDetectResponseMode(
|
||||
workflowStartNode: INode,
|
||||
@@ -133,6 +166,9 @@ export function autoDetectResponseMode(
|
||||
}
|
||||
}
|
||||
|
||||
const chatResponseMode = getChatResponseMode(workflowStartNode, method);
|
||||
if (chatResponseMode) return chatResponseMode;
|
||||
|
||||
// If there are form nodes connected to a current form node we're dealing with a multipage form
|
||||
// and we need to return the formPage response mode when a second page of the form gets submitted
|
||||
// to be able to show potential form errors correctly.
|
||||
@@ -375,7 +411,11 @@ export async function executeWebhook(
|
||||
additionalKeys,
|
||||
);
|
||||
|
||||
if (!['onReceived', 'lastNode', 'responseNode', 'formPage', 'streaming'].includes(responseMode)) {
|
||||
if (
|
||||
!['onReceived', 'lastNode', 'responseNode', 'formPage', 'streaming', 'hostedChat'].includes(
|
||||
responseMode,
|
||||
)
|
||||
) {
|
||||
// If the mode is not known we error. Is probably best like that instead of using
|
||||
// the default that people know as early as possible (probably already testing phase)
|
||||
// that something does not resolve properly.
|
||||
@@ -600,6 +640,8 @@ export async function executeWebhook(
|
||||
didSendResponse = true;
|
||||
}
|
||||
|
||||
didSendResponse = handleHostedChatResponse(res, responseMode, didSendResponse, executionId);
|
||||
|
||||
Container.get(Logger).debug(
|
||||
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
|
||||
{ executionId },
|
||||
|
||||
Reference in New Issue
Block a user