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:
Michael Kret
2025-07-24 11:48:40 +03:00
committed by GitHub
parent e61b25c53f
commit a98ed2ca49
47 changed files with 3441 additions and 71 deletions

View File

@@ -19,6 +19,7 @@ import {
FORM_TRIGGER_NODE_TYPE,
CHAT_TRIGGER_NODE_TYPE,
WAIT_NODE_TYPE,
WAIT_INDEFINITELY,
} from 'n8n-workflow';
import type { Readable } from 'stream';
@@ -334,6 +335,14 @@ export class RespondToWebhook implements INodeType {
],
};
async onMessage(
context: IExecuteFunctions,
_data: INodeExecutionData,
): Promise<INodeExecutionData[][]> {
const inputData = context.getInputData();
return [inputData];
}
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const nodeVersion = this.getNode().typeVersion;
@@ -347,6 +356,10 @@ export class RespondToWebhook implements INodeType {
let response: IN8nHttpFullResponse;
const connectedNodes = this.getParentNodes(this.getNode().name, {
includeNodeParameters: true,
});
const options = this.getNodeParameter('options', 0, {});
const shouldStream =
@@ -354,7 +367,6 @@ export class RespondToWebhook implements INodeType {
try {
if (nodeVersion >= 1.1) {
const connectedNodes = this.getParentNodes(this.getNode().name);
if (!connectedNodes.some(({ type }) => WEBHOOK_NODE_TYPES.includes(type))) {
throw new NodeOperationError(
this.getNode(),
@@ -507,6 +519,40 @@ export class RespondToWebhook implements INodeType {
);
}
const chatTrigger = connectedNodes.find(
(node) => node.type === CHAT_TRIGGER_NODE_TYPE && !node.disabled,
);
const parameters = chatTrigger?.parameters as {
options: { responseMode: string };
};
// if workflow is started from chat trigger and responseMode is set to "responseNodes"
// response to chat will be send by ChatService
if (
chatTrigger &&
!chatTrigger.disabled &&
parameters.options.responseMode === 'responseNodes'
) {
let message = '';
if (responseBody && typeof responseBody === 'object' && !Array.isArray(responseBody)) {
message =
(((responseBody as IDataObject).output ??
(responseBody as IDataObject).text ??
(responseBody as IDataObject).message) as string) ?? '';
if (message === '' && Object.keys(responseBody).length > 0) {
try {
message = JSON.stringify(responseBody, null, 2);
} catch (e) {}
}
}
await this.putExecutionToWait(WAIT_INDEFINITELY);
return [[{ json: {}, sendMessage: message }]];
}
if (
hasHtmlContentType &&
respondWith !== 'text' &&

View File

@@ -8,6 +8,7 @@ import {
type INode,
type INodeExecutionData,
type NodeTypeAndVersion,
CHAT_TRIGGER_NODE_TYPE,
} from 'n8n-workflow';
import { RespondToWebhook } from '../RespondToWebhook.node';
@@ -23,6 +24,78 @@ describe('RespondToWebhook Node', () => {
});
});
describe('chatTrigger response', () => {
it('should handle chatTrigger correctly when enabled and responseBody is an object', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.4 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({
type: CHAT_TRIGGER_NODE_TYPE,
disabled: false,
parameters: { options: { responseMode: 'responseNodes' } },
}),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'responseBody') return { message: 'Hello World' };
if (paramName === 'options') return {};
});
mockExecuteFunctions.putExecutionToWait.mockResolvedValue();
const result = await respondToWebhook.execute.call(mockExecuteFunctions);
expect(result).toEqual([[{ json: {}, sendMessage: 'Hello World' }]]);
});
it('should handle chatTrigger correctly when enabled and responseBody is not an object', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({
type: CHAT_TRIGGER_NODE_TYPE,
disabled: false,
parameters: { options: { responseMode: 'responseNodes' } },
}),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'text';
if (paramName === 'responseBody') return 'Just a string';
if (paramName === 'options') return {};
});
mockExecuteFunctions.putExecutionToWait.mockResolvedValue();
const result = await respondToWebhook.execute.call(mockExecuteFunctions);
expect(result).toEqual([[{ json: {}, sendMessage: '' }]]);
});
it('should not handle chatTrigger when disabled', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
mockExecuteFunctions.getNode.mockReturnValue(mock<INode>({ typeVersion: 1.1 }));
mockExecuteFunctions.getParentNodes.mockReturnValue([
mock<NodeTypeAndVersion>({ type: CHAT_TRIGGER_NODE_TYPE, disabled: true }),
]);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName) => {
if (paramName === 'respondWith') return 'json';
if (paramName === 'responseBody') return { message: 'Hello World' };
if (paramName === 'options') return {};
});
mockExecuteFunctions.sendResponse.mockReturnValue();
await expect(respondToWebhook.execute.call(mockExecuteFunctions)).resolves.not.toThrow();
expect(mockExecuteFunctions.sendResponse).toHaveBeenCalled();
});
it('should return input data onMessage call', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { input: true } }]);
const result = await respondToWebhook.onMessage(mockExecuteFunctions, {
json: { message: '' },
});
expect(result).toEqual([[{ json: { input: true } }]]);
});
});
describe('execute method', () => {
it('should throw an error if no WEBHOOK_NODE_TYPES in parents', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([]);