mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Refactor ChatTrigger node and update imports for compatibility
• Update import paths • Implement INodeType interface • Move handleFormData to separate function • Adjust webhook function implementation • Fix error handling and response formatting to always return empty array
This commit is contained in:
parent
6cd9b996af
commit
8a240380f4
|
@ -1,6 +1,7 @@
|
||||||
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
import type { Serialized } from '@langchain/core/load/serializable';
|
||||||
|
import type { BaseChatMemory } from 'langchain/memory/chat_memory';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { Node, NodeConnectionType, commonCORSParameters } from 'n8n-workflow';
|
import { NodeConnectionType, commonCORSParameters } from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
|
@ -10,6 +11,7 @@ import type {
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
|
INodeType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { validateAuth } from './GenericFunctions';
|
import { validateAuth } from './GenericFunctions';
|
||||||
|
@ -34,7 +36,69 @@ const allowedFileMimeTypeOption: INodeProperties = {
|
||||||
'Allowed file types for upload. Comma-separated list of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types" target="_blank">MIME types</a>.',
|
'Allowed file types for upload. Comma-separated list of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types" target="_blank">MIME types</a>.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ChatTrigger extends Node {
|
async function handleFormData(context: IWebhookFunctions) {
|
||||||
|
const req = context.getRequestObject() as MultiPartFormData.Request;
|
||||||
|
const options = context.getNodeParameter('options', {}) as IDataObject;
|
||||||
|
const { data, files } = req.body;
|
||||||
|
|
||||||
|
const returnItem: INodeExecutionData = {
|
||||||
|
json: data,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (files && Object.keys(files).length) {
|
||||||
|
returnItem.json.files = [] as Array<Omit<IBinaryData, 'data'>>;
|
||||||
|
returnItem.binary = {};
|
||||||
|
|
||||||
|
const count = 0;
|
||||||
|
for (const fileKey of Object.keys(files)) {
|
||||||
|
const processedFiles: MultiPartFormData.File[] = [];
|
||||||
|
if (Array.isArray(files[fileKey])) {
|
||||||
|
processedFiles.push(...files[fileKey]);
|
||||||
|
} else {
|
||||||
|
processedFiles.push(files[fileKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileIndex = 0;
|
||||||
|
for (const file of processedFiles) {
|
||||||
|
let binaryPropertyName = 'data';
|
||||||
|
|
||||||
|
// Remove the '[]' suffix from the binaryPropertyName if it exists
|
||||||
|
if (binaryPropertyName.endsWith('[]')) {
|
||||||
|
binaryPropertyName = binaryPropertyName.slice(0, -2);
|
||||||
|
}
|
||||||
|
if (options.binaryPropertyName) {
|
||||||
|
binaryPropertyName = `${options.binaryPropertyName.toString()}${count}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryFile = await context.nodeHelpers.copyBinaryFile(
|
||||||
|
file.filepath,
|
||||||
|
file.originalFilename ?? file.newFilename,
|
||||||
|
file.mimetype,
|
||||||
|
);
|
||||||
|
|
||||||
|
const binaryKey = `${binaryPropertyName}${fileIndex}`;
|
||||||
|
|
||||||
|
const binaryInfo = {
|
||||||
|
...pick(binaryFile, ['fileName', 'fileSize', 'fileType', 'mimeType', 'fileExtension']),
|
||||||
|
binaryKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
returnItem.binary = Object.assign(returnItem.binary ?? {}, {
|
||||||
|
[`${binaryKey}`]: binaryFile,
|
||||||
|
});
|
||||||
|
returnItem.json.files = [
|
||||||
|
...(returnItem.json.files as Array<Omit<IBinaryData, 'data'>>),
|
||||||
|
binaryInfo,
|
||||||
|
];
|
||||||
|
fileIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChatTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
displayName: 'Chat Trigger',
|
displayName: 'Chat Trigger',
|
||||||
name: 'chatTrigger',
|
name: 'chatTrigger',
|
||||||
|
@ -378,73 +442,11 @@ export class ChatTrigger extends Node {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
private async handleFormData(context: IWebhookFunctions) {
|
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||||
const req = context.getRequestObject() as MultiPartFormData.Request;
|
const res = this.getResponseObject();
|
||||||
const options = context.getNodeParameter('options', {}) as IDataObject;
|
|
||||||
const { data, files } = req.body;
|
|
||||||
|
|
||||||
const returnItem: INodeExecutionData = {
|
const isPublic = this.getNodeParameter('public', false) as boolean;
|
||||||
json: data,
|
const nodeMode = this.getNodeParameter('mode', 'hostedChat') as string;
|
||||||
};
|
|
||||||
|
|
||||||
if (files && Object.keys(files).length) {
|
|
||||||
returnItem.json.files = [] as Array<Omit<IBinaryData, 'data'>>;
|
|
||||||
returnItem.binary = {};
|
|
||||||
|
|
||||||
const count = 0;
|
|
||||||
for (const fileKey of Object.keys(files)) {
|
|
||||||
const processedFiles: MultiPartFormData.File[] = [];
|
|
||||||
if (Array.isArray(files[fileKey])) {
|
|
||||||
processedFiles.push(...files[fileKey]);
|
|
||||||
} else {
|
|
||||||
processedFiles.push(files[fileKey]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileIndex = 0;
|
|
||||||
for (const file of processedFiles) {
|
|
||||||
let binaryPropertyName = 'data';
|
|
||||||
|
|
||||||
// Remove the '[]' suffix from the binaryPropertyName if it exists
|
|
||||||
if (binaryPropertyName.endsWith('[]')) {
|
|
||||||
binaryPropertyName = binaryPropertyName.slice(0, -2);
|
|
||||||
}
|
|
||||||
if (options.binaryPropertyName) {
|
|
||||||
binaryPropertyName = `${options.binaryPropertyName.toString()}${count}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const binaryFile = await context.nodeHelpers.copyBinaryFile(
|
|
||||||
file.filepath,
|
|
||||||
file.originalFilename ?? file.newFilename,
|
|
||||||
file.mimetype,
|
|
||||||
);
|
|
||||||
|
|
||||||
const binaryKey = `${binaryPropertyName}${fileIndex}`;
|
|
||||||
|
|
||||||
const binaryInfo = {
|
|
||||||
...pick(binaryFile, ['fileName', 'fileSize', 'fileType', 'mimeType', 'fileExtension']),
|
|
||||||
binaryKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
returnItem.binary = Object.assign(returnItem.binary ?? {}, {
|
|
||||||
[`${binaryKey}`]: binaryFile,
|
|
||||||
});
|
|
||||||
returnItem.json.files = [
|
|
||||||
...(returnItem.json.files as Array<Omit<IBinaryData, 'data'>>),
|
|
||||||
binaryInfo,
|
|
||||||
];
|
|
||||||
fileIndex += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
async webhook(ctx: IWebhookFunctions): Promise<IWebhookResponseData> {
|
|
||||||
const res = ctx.getResponseObject();
|
|
||||||
|
|
||||||
const isPublic = ctx.getNodeParameter('public', false) as boolean;
|
|
||||||
const nodeMode = ctx.getNodeParameter('mode', 'hostedChat') as string;
|
|
||||||
if (!isPublic) {
|
if (!isPublic) {
|
||||||
res.status(404).end();
|
res.status(404).end();
|
||||||
return {
|
return {
|
||||||
|
@ -452,7 +454,7 @@ export class ChatTrigger extends Node {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = ctx.getNodeParameter('options', {}) as {
|
const options = this.getNodeParameter('options', {}) as {
|
||||||
getStarted?: string;
|
getStarted?: string;
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
loadPreviousSession?: LoadPreviousSessionChatOption;
|
loadPreviousSession?: LoadPreviousSessionChatOption;
|
||||||
|
@ -463,14 +465,15 @@ export class ChatTrigger extends Node {
|
||||||
allowedFilesMimeTypes?: string;
|
allowedFilesMimeTypes?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const req = ctx.getRequestObject();
|
const req = this.getRequestObject();
|
||||||
const webhookName = ctx.getWebhookName();
|
const webhookName = this.getWebhookName();
|
||||||
const mode = ctx.getMode() === 'manual' ? 'test' : 'production';
|
const mode = this.getMode() === 'manual' ? 'test' : 'production';
|
||||||
const bodyData = ctx.getBodyData() ?? {};
|
const bodyData = this.getBodyData() ?? {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await validateAuth(ctx);
|
await validateAuth(this);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log('🚀 ~ ChatTrigger ~ webhook ~ error:', error);
|
||||||
if (error) {
|
if (error) {
|
||||||
res.writeHead((error as IDataObject).responseCode as number, {
|
res.writeHead((error as IDataObject).responseCode as number, {
|
||||||
'www-authenticate': 'Basic realm="Webhook"',
|
'www-authenticate': 'Basic realm="Webhook"',
|
||||||
|
@ -483,19 +486,19 @@ export class ChatTrigger extends Node {
|
||||||
if (nodeMode === 'hostedChat') {
|
if (nodeMode === 'hostedChat') {
|
||||||
// Show the chat on GET request
|
// Show the chat on GET request
|
||||||
if (webhookName === 'setup') {
|
if (webhookName === 'setup') {
|
||||||
const webhookUrlRaw = ctx.getNodeWebhookUrl('default') as string;
|
const webhookUrlRaw = this.getNodeWebhookUrl('default') as string;
|
||||||
const webhookUrl =
|
const webhookUrl =
|
||||||
mode === 'test' ? webhookUrlRaw.replace('/webhook', '/webhook-test') : webhookUrlRaw;
|
mode === 'test' ? webhookUrlRaw.replace('/webhook', '/webhook-test') : webhookUrlRaw;
|
||||||
const authentication = ctx.getNodeParameter('authentication') as
|
const authentication = this.getNodeParameter('authentication') as
|
||||||
| 'none'
|
| 'none'
|
||||||
| 'basicAuth'
|
| 'basicAuth'
|
||||||
| 'n8nUserAuth';
|
| 'n8nUserAuth';
|
||||||
const initialMessagesRaw = ctx.getNodeParameter('initialMessages', '') as string;
|
const initialMessagesRaw = this.getNodeParameter('initialMessages', '') as string;
|
||||||
const initialMessages = initialMessagesRaw
|
const initialMessages = initialMessagesRaw
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => line)
|
.filter((line) => line)
|
||||||
.map((line) => line.trim());
|
.map((line) => line.trim());
|
||||||
const instanceId = ctx.getInstanceId();
|
const instanceId = this.getInstanceId();
|
||||||
|
|
||||||
const i18nConfig = pick(options, ['getStarted', 'inputPlaceholder', 'subtitle', 'title']);
|
const i18nConfig = pick(options, ['getStarted', 'inputPlaceholder', 'subtitle', 'title']);
|
||||||
|
|
||||||
|
@ -514,7 +517,9 @@ export class ChatTrigger extends Node {
|
||||||
allowedFilesMimeTypes: options.allowedFilesMimeTypes,
|
allowedFilesMimeTypes: options.allowedFilesMimeTypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).send(page).end();
|
res.status(200);
|
||||||
|
res.send(page);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
noWebhookResponse: true,
|
noWebhookResponse: true,
|
||||||
};
|
};
|
||||||
|
@ -522,28 +527,31 @@ export class ChatTrigger extends Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodyData.action === 'loadPreviousSession') {
|
if (bodyData.action === 'loadPreviousSession') {
|
||||||
|
const webhookResponse: { data: Serialized[]; error?: unknown } = { data: [] };
|
||||||
if (options?.loadPreviousSession === 'memory') {
|
if (options?.loadPreviousSession === 'memory') {
|
||||||
const memory = (await ctx.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as
|
try {
|
||||||
|
const memory = (await this.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as
|
||||||
| BaseChatMemory
|
| BaseChatMemory
|
||||||
| undefined;
|
| undefined;
|
||||||
const messages = ((await memory?.chatHistory.getMessages()) ?? [])
|
|
||||||
|
webhookResponse.data = ((await memory?.chatHistory.getMessages()) ?? [])
|
||||||
.filter((message) => !message?.additional_kwargs?.hideFromUI)
|
.filter((message) => !message?.additional_kwargs?.hideFromUI)
|
||||||
.map((message) => message?.toJSON());
|
.map((message) => message?.toJSON());
|
||||||
return {
|
} catch (error) {
|
||||||
webhookResponse: { data: messages },
|
webhookResponse.error = error.message;
|
||||||
};
|
this.logger.error(`Could not retrieve memory for loadPreviousSession: ${error.message}`);
|
||||||
} else if (options?.loadPreviousSession === 'notSupported') {
|
} finally {
|
||||||
// If messages of a previous session should not be loaded, simply return an empty array
|
return { webhookResponse };
|
||||||
return {
|
|
||||||
webhookResponse: { data: [] },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { webhookResponse };
|
||||||
|
}
|
||||||
|
|
||||||
let returnData: INodeExecutionData[];
|
let returnData: INodeExecutionData[];
|
||||||
const webhookResponse: IDataObject = { status: 200 };
|
const webhookResponse: IDataObject = { status: 200 };
|
||||||
if (req.contentType === 'multipart/form-data') {
|
if (req.contentType === 'multipart/form-data') {
|
||||||
returnData = [await this.handleFormData(ctx)];
|
returnData = [await handleFormData(this)];
|
||||||
return {
|
return {
|
||||||
webhookResponse,
|
webhookResponse,
|
||||||
workflowData: [returnData],
|
workflowData: [returnData],
|
||||||
|
@ -554,7 +562,7 @@ export class ChatTrigger extends Node {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
webhookResponse,
|
webhookResponse,
|
||||||
workflowData: [ctx.helpers.returnJsonArray(returnData)],
|
workflowData: [this.helpers.returnJsonArray(returnData)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue